上一节完成了简单的动态脚本的实现,但是还只能给变量赋值,打印变量,整数。本节v0.0.6的版本,让动态脚本能够做一些更有用的东东,在linux系统下有一种bc的命令行下的小工具,可以实现加减乘除的运算,还...
上一节完成了简单的动态脚本的实现,但是还只能给变量赋值,打印变量,整数。本节v0.0.6的版本,让动态脚本能够做一些更有用的东东,在linux系统下有一种bc的命令行下的小工具,可以实现加减乘除的运算,还可以进行简单的编程,所以本节的代码里将加入加减乘除的运算功能,以及字符串的打印等功能。
本节v0.0.6版本的源代码下载地址为: http://pan.baidu.com/share/link?shareid=82546&uk=940392313 (此为百度云盘的共享链接地址) , 访问该地址可以看到三个文件: zengl_lang_v0.0.6_forXP.rar (XP系统下的vs2008解决方案和源代码), zengl_language_v0.0.6_forLinux.tar.gz (Linux系统下的源代码和makefile) ,v0.0.6-v0.0.5-diffs.txt (v0.0.6和v0.0.5的代码变化情况)。
我在SourceForge.net上开了个project,地址为:https://sourceforge.net/projects/zengl/files/ 从里面可以看到各个版本的代码压缩包,例如本节的zengl_lang_v0.0.6_forXP.rar ,zengl_language_v0.0.6_forLinux.tar.gz,v0.0.6-v0.0.5-diffs.txt
先来看下本版本的描述 (在linux代码包里的usage.txt里有这段描述):
v0.0.6版本,该版本由简单的赋值打印整数的指令发展到可以加减乘除以及字符串的打印等操作,完成了类似bc的小型的计算器功能。
assemble.c在原来的mov,print指令的基础上增加了plus,minis,times,divide,加减乘除的指令,另外还有加减乘除递归时要用到的push,pop栈操作指令用以保存和恢复AX用的。func.c中修复了zl_realloc函数中可能出现的段错误的BUG。 global.h头文件中对应增加了加减乘除对应的TOKENTYPE,以及其他一些必要的宏和类型。
main.c中的getToken里增加了浮点数的token的判断获取。在parser.c中将浮点数和字符串的token也增加到语法数中,从而在后面的操作中可以对浮点数和字符串进行操作。
run.c文件中将MEM_LIST内存结构由原来的int整形数转为由整形,浮点,指针等构成的联合体,这样虚拟内存结构中就可以存放各种需要的数据,就可以方便的进行整数,浮点,字符串的运算,在getToken中加入了字符串的获取,getInst中增加了加减乘除等指令需要的src,dest等指令信息。在RunInsts函数中增加了加减乘除相关的具体执行过程,除了整数,浮点数可以进行加减乘除运算外,字符串也可以和字符串或别的数字进行加法运算,打印语句可以打印变量,数字,字符串。run.h中也对应增加了些需要的枚举类型等。变量的类型是在赋值操作时动态设置的。
在symbol.c中有个没用的类型idtype,暂时没用(因为类型是动态执行时才能确定的),先放着,也许以后会用到。
作者:zenglong
时间:2012年2月5日 (因为比较忙,2月份写的,一直到现在才发布)
在源代码文件里包含了很多注释,方便调试分析研究,先来看下几个主要文件源代码的变化情况。
在main.c文件里主要增加了浮点数的判断:
enum TOKENTYPE getToken()
{
...... //省略N行代码
case INNUM: //在INNUM状态下,一直读取字符,直到该字符不是数字为止,并将读取的单个数字通过makeTokenStr构成完整的数值。
if(isdigit(ch))
makeTokenStr(ch);
else if(ch == '.') //如果是小数点,就说明是浮点数,进入INFLOAT状态
{
makeTokenStr(ch);
state = INFLOAT;
}
else
{
state = DOWN;
token = NUM; //将token设为NUM表示读取到了一个数字。
ungetchar();
}
break;
case INFLOAT: //在INFLOAT状态下,一直读取字符,直到该字符不是数字为止,这样就可以得到一个浮点数。
if(isdigit(ch))
makeTokenStr(ch);
else
{
state = DOWN;
token = FLOAT;
ungetchar();
}
break;
...... //省略N行代码
}
在parser.c里主要在语法分析时增加了对字符串和浮点数的判断。例如express函数:
int express()
{
...... //省略N行代码
case INPLUS_MINIS:
if(ISTOKTYPEOR4(ID,NUM,FLOAT,STR) && ISTOKTYPEXOR(curnode-1,PLUS,MINIS)) //加减运算符右边是ID标示符或数字或浮点数或字符串时,将ID加入运算符的子节点。
{
if(ISTOKTYPE(STR))
{
if(ISTOKTYPEX(curnode-1,PLUS)) ; //加减运算符,如果是加法就可以参与字符串运算
else
syntax_error("string can only be used with plus now (字符串暂时只能和加法进行运算)"); //语法错误
}
AddNodeChild(p,curnode);
}
...... //省略N行代码
}
在assemble.c文件里增加了对加减乘除以及堆栈操作指令的汇编代码的输出:
/**
该函数根据AST抽象语法树的节点索引将某节点转为汇编代码,并通过outcode函数输出到汇编代码文件里。
参数nodenum为节点在语法树动态数组里的节点索引。
*/
void gen_codes(int nodenum)
{
....... //省略N行代码
case INTIME_DIVIDE: //乘除运算符汇编输出
case INPLUS_MINIS: //加减运算符汇编输出
if(ISTOKCOUNT(nodenum,2)) //判断节点是否有两个子节点
{
chnum = nodes[nodenum].childs.childnum; //获取子节点的索引数组
if(ISTOKTYPEX(chnum[0],ID)) //以下为对第一个子节点的判断并输出相应的汇编代码,结果储存在AX寄存器中。
{
sym = lookupSym(chnum[0]);
outcode("MOV AX (%d)",sym->memloc);
}
else if(ISTOKTYPEXOR(chnum[0],NUM,FLOAT))
outcode("MOV AX %s",GETTOKSTR(chnum[0]));
else if(ISTOKTYPEX(chnum[0],STR) && ISTOKTYPEX(nodenum,PLUS)) //当第一个子节点是字符串时,只有当前节点为加号时才输出汇编码,因为目前只有加号可以参与字符串的运算,字符串相加就是两字符串连接在一起。
outcode("MOV AX "%s"",GETTOKSTR(chnum[0]));
else if(ISTOKEXPRESS(chnum[0]))
gen_codes(chnum[0]);
else
gencode_error("gen code in time divide plus minis op err, first child is not id,num,float,string or express (加减乘除运算符的第一个子节点不是变量或数字或字符串,也不是表达式运算符,提示:字符串目前只能用于加法) ",nodenum); //汇编输出异常警告信息
if(ISTOKTYPEX(chnum[1],ID)) //以下为对第二个子节点的判断并输出相应的汇编代码,结果储存在BX寄存器中。
{
sym = lookupSym(chnum[1]);
outcode("MOV BX (%d)",sym->memloc);
}
else if(ISTOKTYPEXOR(chnum[1],NUM,FLOAT))
outcode("MOV BX %s",GETTOKSTR(chnum[1]));
else if(ISTOKTYPEX(chnum[1],STR) && ISTOKTYPEX(nodenum,PLUS)) //当第一个子节点是字符串时,只有当前节点为加号时才输出汇编码,原因同上。
outcode("MOV BX "%s"",GETTOKSTR(chnum[1]));
else if(ISTOKEXPRESS(chnum[1]))
{
outcode("PUSH AX"); //将前面的AX寄存器里的值压入栈,下面的gen_codes会将新值传入AX
gen_codes(chnum[1]);
outcode("MOV BX AX"); //将AX里存放的表达式的结果转存到BX寄存器中。
outcode("POP AX"); //从虚拟栈中弹出AX值,下面就会利用AX和BX的值进行加减乘除运算。
}
else
gencode_error("gen code in time divide plus minis op err, second child is not id,num,float,string or express (加减乘除运算符的第二个子节点不是变量或数字或字符串,也不是表达式运算符,提示:字符串目前只能用于加法)",nodenum); //汇编输出异常警告信息
switch(nodes[nodenum].toktype) //根据运算符节点的类型输出不同的汇编代码。
{
case PLUS:
outcode("PLUS"); //加法汇编码,PLUS指令会将AX 和 BX寄存器的值相加,结果存放在AX中。
break;
case MINIS:
outcode("MINIS"); //减法汇编码,MINIS指令会将AX 和 BX寄存器的值相减,结果存放在AX中。
break;
case TIMES:
outcode("TIMES"); //乘法汇编码,TIMES指令会将AX 和 BX寄存器的值相乘,结果存放在AX中。
break;
case DIVIDE:
outcode("DIVIDE"); //除法汇编码,DIVIDE指令会将AX 和 BX寄存器的值相除,结果存放在AX中。
break;
}
state = DOWN; //加减乘除操作的代码生成完毕,state设为down,接着退出循环,完成一次gen_codes操作。
}
else
gencode_error("gen code err: time divide plus minis op node must have 2 child-nodes (加减乘除运算符必须包含两个子节点)",nodenum); //汇编输出异常警告信息
break;
....... //省略N行代码
}
在run.c中主要增加了对加减乘除及堆栈操作汇编指令的执行处理过程:
在run.c的开头:char * gl_ops[]={"MOV","print","PLUS","MINIS","TIMES","DIVIDE","PUSH","POP","END",NULL}; //当前虚拟机支持的操作指令,MOV指令,print打印指令,加减乘除运算符指令等等,END程序结束指令等。PUSH POP 为堆栈操作指令
char * gl_regs[]={"AX","BX",NULL}; //在汇编代码中可能会出现虚拟机寄存器类型,因为PC不会出现在代码文件中,所以没有PC寄存器,此版本增加了BX寄存器
在run.c的RunInsts函数里对应增加了对这些指令的处理过程:
/**
虚拟机执行汇编指令的主程式。
*/
void RunInsts()
{
....... //省略N行代码
case PLUS: //加法指令
if(REG(AX).idtype == IDINT && REG(BX).idtype == IDINT) //加法指令AX BX都是整数时,相加
REG(AX).val.dword += REG(BX).val.dword;
else if(REG(AX).idtype == IDSTR)
{
if(REG(BX).idtype == IDINT) //加法指令,AX 为字符串,BX为整数,将整数转为字符串,再添加到AX字符串的末尾
{
sprintf(tmpchar,"%d",REG(BX).val.dword); //将整数转为字符串
lentmp = strlen(tmpchar); //得到整数字符串的长度
len = strlen(REG(AX).val.ptr) + lentmp; //得到AX字符串加整数字符串的总长度
REG(AX).val.ptr = zl_realloc(REG(AX).val.ptr,(len+1)*sizeof(char)); //为AX分配足够的空间。
strncat(REG(AX).val.ptr,tmpchar,lentmp); //strncat执行安全的连接。strcat有可能会破坏内存池。
}
else if(REG(BX).idtype == IDFLOAT) //加法指令,AX 为字符串,BX为浮点数,将浮点数转为字符串,再添加到AX字符串的末尾
{
sprintf(tmpchar,"%f",REG(BX).val.qword);
lentmp = strlen(tmpchar);
len = strlen(REG(AX).val.ptr) + lentmp;
REG(AX).val.ptr = zl_realloc(REG(AX).val.ptr,(len+1)*sizeof(char));
strncat(REG(AX).val.ptr,tmpchar,lentmp);
}
else if(REG(BX).idtype == IDSTR) //加法指令,AX 为字符串,BX也为字符串,将BX字符串添加到AX字符串的末尾
{
lentmp = strlen((char *)REG(BX).val.ptr);
len = strlen((char *)REG(AX).val.ptr) + lentmp;
REG(AX).val.ptr = zl_realloc(REG(AX).val.ptr,(len+1)*sizeof(char));
strncat((char *)REG(AX).val.ptr,(char *)REG(BX).val.ptr,lentmp);
}
}
else if(REG(BX).idtype == IDSTR)
{
if(REG(AX).idtype == IDINT) //加法指令,BX为字符串,AX为整数,将AX转为字符串,再和BX字符串相连接。
{
sprintf(tmpchar,"%d",REG(AX).val.dword);
lentmp = strlen(tmpchar);
len = strlen(REG(BX).val.ptr) + lentmp;
REG(AX).val.ptr = zl_realloc(REG(AX).val.ptr,(len+1)*sizeof(char));
strncpy(REG(AX).val.ptr,tmpchar,lentmp);
((char *)REG(AX).val.ptr)[lentmp] = STRNULL;
strncat(REG(AX).val.ptr,REG(BX).val.ptr,len-lentmp);
}
else if(REG(AX).idtype == IDFLOAT) //加法指令,BX为字符串,AX为浮点数,将AX转为字符串,再和BX字符串相连接。
{
sprintf(tmpchar,"%f",REG(AX).val.qword);
lentmp = strlen(tmpchar);
len = strlen(REG(BX).val.ptr) + lentmp;
REG(AX).val.ptr = zl_realloc(REG(AX).val.ptr,(len+1)*sizeof(char));
strncpy(REG(AX).val.ptr,tmpchar,lentmp);
((char *)REG(AX).val.ptr)[lentmp] = STRNULL;
strncat(REG(AX).val.ptr,REG(BX).val.ptr,len-lentmp);
}
REG(AX).idtype = IDSTR;
}
else if(REG(AX).idtype == IDFLOAT)
{
if(REG(BX).idtype == IDFLOAT) //加法指令,AX为浮点数,BX也为浮点数,AX BX浮点数相加,结果存放在AX的qword成员里。
REG(AX).val.qword += REG(BX).val.qword;
else if(REG(BX).idtype == IDINT) //加法指令,AX为浮点数,BX为整数,AX BX相加,结果存放在AX的qword成员里。
REG(AX).val.qword += REG(BX).val.dword;
}
else if(REG(BX).idtype == IDFLOAT && REG(AX).idtype == IDINT) //加法指令,AX为整数,BX为浮点数,AX BX相加,结果存放在AX的qword成员里,将AX类型设为浮点数。
{
REG(AX).val.qword = REG(AX).val.dword + REG(BX).val.qword;
REG(AX).idtype = IDFLOAT;
}
break;
....... //省略N行代码
case PUSH: //堆栈的压栈操作。将数据存入堆栈空间。
if(CUR_INST.src.type == REG) //压栈源操作数为寄存器类型
{
if(REG( CUR_INST.src.val.reg ).idtype == IDINT)
{
tmpmem.val.dword = REGVAL( CUR_INST.src.val.reg ).dword;
StackOps(ADDMEM_INT,tmpmem); //利用堆栈操作函数来操作堆栈空间的数据。
}
else if(REG( CUR_INST.src.val.reg ).idtype == IDFLOAT)
{
tmpmem.val.qword = REGVAL( CUR_INST.src.val.reg ).qword;
StackOps(ADDMEM_DOUBLE,tmpmem);
}
else if(REG( CUR_INST.src.val.reg ).idtype == IDSTR)
{
tmpmem.val.ptr = REGVAL( CUR_INST.src.val.reg ).ptr;
StackOps(ADDMEM_PTR,tmpmem);
}
}
break;
case POP: //堆栈的弹出栈操作,将数据从堆栈中弹出去。
if(CUR_INST.src.type == REG)
{
tmpmem = StackOps(GETMEM,tmpmem); //获取堆栈最后一个数据。
if(tmpmem.idtype == IDINT) //堆栈数据为整数时。
{
REGVAL( CUR_INST.src.val.reg ).dword = tmpmem.val.dword; //将堆栈的整数设置到目标寄存器里,pop只有一个操作数,所以这里用src表示目标。
REG( CUR_INST.src.val.reg ).idtype = IDINT;
}
else if(tmpmem.idtype == IDFLOAT) //堆栈数据为浮点数时。
{
REGVAL( CUR_INST.src.val.reg ).qword = tmpmem.val.qword;
REG( CUR_INST.src.val.reg ).idtype = IDFLOAT;
}
else if(tmpmem.idtype == IDSTR) //堆栈数据为字符串时。
{
tmptr = ®VAL(CUR_INST.src.val.reg).ptr; //得到寄存器字符串指针的二级指针。
len = strlen(tmpmem.val.ptr);
(*tmptr) = zl_realloc((*tmptr),(len+1) * sizeof(char));
strncpy(*tmptr,tmpmem.val.ptr,len);
(*((char **)tmptr))[len] = STRNULL;
REG( CUR_INST.src.val.reg ).idtype = IDSTR;
}
}
break;
....... //省略N行代码
}
这里列举了RunInsts函数的加法指令和PUSH , POP栈操作指令的执行过程。堆栈的概念和汇编语言里的堆栈概念是一致的,就是存放临时数据和恢复寄存器数据的地方。
下面举例来将本节里抽象的函数代码转为让人可理解的东东。
以本节的test.zl测试脚本为例,test.zl的代码如下(代码注释是这里为了说明而加的,在脚本文件里并没有,因为目前zengl脚本还没加入注释功能,在以后版本里会加入该功能):
a = 5; //将5赋值给a变量
b = a * 2 + 116; //将a变量和2相乘,再与116相加,最后将计算结果赋值给变量b
str = 'a is ' + a + ' b is ' + b; //通过加法运算符将字符串和变量连接在一起,并赋值给str变量
print str; //打印字符串变量str,因为目前print指令还只支持直接打印变量,整数,浮点数和字符串,还不支持打印加减等表达式。
这段代码经过main.c词法扫描,parser.c语法分析后生成的语法树如下:
经过assemble.c汇编输出后,将会生成如下的汇编代码(注释只是这里为了说明而添加的,在原.zlc的代码文件中并没有,因为目前还没添加注释功能):
0 MOV AX 5; //第一个0是PC寄存器索引,用于指示当前虚拟机执行哪条指令的一个索引,MOV AX 5指将整数5赋值给AX寄存器,汇编指令以分号结束。
1 MOV (0) AX; //将AX寄存器的值赋值给变量a所在的内存位置,(0)表示全局虚拟内存地址为0的位置。第一条和这一条指令共同完成a = 5;这条原脚本语句。
2 MOV AX (0); //将变量a所在的内存0的值再传回给AX寄存器,因为下面的TIMES指令需要AX BX两个寄存器来进行乘法运算。
3 MOV BX 2; //将2赋值给BX寄存器。
4 TIMES; //将AX和BX的值相乘,结果存放在AX寄存器中。2,3,4三条指令共同完成了 a * 2的运算。
5 MOV BX 116; //将116赋值给BX寄存器,用于下面的加法运算。
6 PLUS; //将AX和BX相加,AX存放的是前面a*2的结果,BX存放的是116,所以4,5,6完成了a*2+116的运算。结果同样存放在AX寄存器中。
7 MOV (1) AX; //将AX的值赋值给变量b所在的内存位置1处,这样就完成了 b = a*2 + 116 的脚本语句。
8 MOV AX "a is "; //将'a is '这个字符串赋值给AX寄存器。
9 MOV BX (0); //将变量a的值赋值给BX。
10 PLUS; //将AX,BX相加。完成了'a is '+a的语句。
11 MOV BX " b is "; //将' b is ' 字符串赋值给BX寄存器。
12 PLUS; //将AX,BX相加。完成了'a is '+a+' b is ' 的运算。
13 MOV BX (1); //将变量b赋值给BX。
14 PLUS; //将AX,BX相加。完成了'a is '+a+' b is ' +b的运算。结果存放在AX里。
15 MOV (2) AX; //将AX赋值给变量str的内存地址2处。
16 print (2); //将变量str打印出来。
17 END; //程序结束。退出虚拟机。
最后这段生成的汇编代码存放在.zlc为后缀的文件中,经过run.c虚拟机解释执行就会输出如下结果:
这样一个完整的小型计算器脚本就执行完了。在输出结果里开头的都是指令的一些详细信息,如果不想输出这些信息,只想查看结果,可以在run.c的main函数里将下面的这条语句注释掉:
void main(int argc,char * argv[])
{
......... //省略N行代码
printInstList(); //打印gl_codes里存放的所有指令的详细信息。将这条语句注释掉,就可以不在开头输出详细信息。
......... //省略N行代码
}
对于windows用户,请确保在项目属性的配置里,命令参数配置的是test.zl(对于zengl_lang_v0.0.6的项目)或test.zlc(对于zenglrun的项目)
对于其他代码的分析,因为已经在C源代码里加了很多注释,请结合注释,加上VS2008之类的调试开发环境进行调试分析。
linux系统下的用户请结合usage.txt的说明,先运行make clean 将原来生成的zengl zenglrun 和 main.o parser.o assemble.o func.o run.o symbol.o文件删除。
再运行make all (单纯的make只能生成zengl,所以需要make all来生成所有的目标)
生成zengl zenglrun 和 main.o parser.o assemble.o func.o run.o symbol.o。(在生成过程中如果出现一些警告,暂不管他)
最后运行 ./zengl test.zl 查看printASTnodes函数打印抽象语法树节点的结果,以及符号表输出的变量信息(例如变量的内存地址,以及在源文件的行列号等)。
接着运行./zenglrun test.zlc (注意是.zlc结尾的文件名,因为zenglrun虚拟机只能运行.zlc里的汇编代码)。
本例的抽象语法树等很多高级的原理都可以在《龙书》中找到。
最后的最后,如果转载请注明来源 http://www.zengl.com , OK , 先到这里,休息,休息一下 O(∩_∩)O~