前几节实现了流程控制结构,如if条件选择,for循环控制等,但是编程经常需要将常用的代码做成模块的形式,方便调用和维护,通常的一个做法就是使用函 数,将有用的代码放在一个函数定义里,需...

    前几节实现了流程控制结构,如if条件选择,for循环控制等,但是编程经常需要将常用的代码做成模块的形式,方便调用和维护,通常的一个做法就是使用函 数,将有用的代码放在一个函数定义里,需要的时候直接调用该函数即可,所以这一节,我们来实现一下函数(在面向对象的编程语言里喜欢叫做方法)。

    还是一样的开场白,先介绍一下本节v0.0.12版本的源代码的下载地址:http://pan.baidu.com/share/link?shareid=165884&uk=940392313  (此为百度云盘的共享链接地址),访问该地址可以看到三个文件:zengl_lang_v0.0.12_forXP.rar (XP系统下的vs2008解决方案和源代码), zengl_language_v0.0.12_forLinux.tar.gz  (Linux系统下的源代码和makefile) ,v0.0.12-v0.0.11-diffs.txt  (v0.0.12和v0.0.11的代码变化情况)。

    SourceForge.net上的仓库地址为:https://sourceforge.net/projects/zengl/files/   从里面可以看到各个版本的代码压缩包,比如本节的zengl_lang_v0.0.12_forXP.rar zengl_language_v0.0.12_forLinux.tar.gz,v0.0.12-v0.0.11-diffs.txt 。

    先来看下本版本的描述 (在linux代码包里的usage.txt里有这段描述,在最近几个带有git版本的....diffs.txt和git log中也有这段描述

    v0.0.12版本,该版本实现了FUN函数结构,以及FUNCALL函数调用的实现。
   
    和其他控制结构一样,FUN和FUNCALL的语法树都是在parser.c中生成,在express2函数里通过增加ID标识符紧跟左括号的处理实现FUNCALL的语法树,在statement中FUN语法树的生成和FOR,IF的大同小异。
    main.c中添加了fun,endfun的保留字的token。
    symbol.c中在原来全局变量Hash表的基础上,增加了FUN函数名的Hash表以及函数里面的局部local变量的hash表。这样在后面的汇编和链接中就可以在hash表中查询到对应函数的函数id,以及函数里的局部变量的索引值。
    难点在于assemble.c生成汇编代码部分,通过增加ARG,LOC寄存器来访问栈空间里的参数和局部变量。在调用某个函数时,先将原函数的 ARG,LOC压入栈,紧接着将参数压入栈,并将参数位置先保存在ARGTMP中,最后要跳转时再赋给ARG,因为在生成参数时还可能要用到原来的ARG 参数。
    在参数压入栈后,再将返回地址压入栈,最后设置ARG,LOC寄存器,接着跳转到相应的函数去。
    在FUN函数里,开头需要一个JMP跳转到函数RET后的地址,这样在没有调用函数时就不会执行FUN里的代码,而是执行FUN后面的代码,当 FUNCALL调用此函数时,只要跳转到第一条JMP语句后即可执行该函数。在FUN的第一个JMP后紧跟着FUNARG指令,FUNARG指令后面接的 是该函数定义的参数个数,当调用函数所提供的实参少于函数定义的参数数目时,将自动压入0到栈中,使得参数一致。
    在FUNARG前会调用Symbol.c中的ScanFunArg函数将扫描到的参数添加到参数Hash表中,参数和局部变量共用一个hash表。在 FUNARG指令后会调用Symbol.c中的ScanFunLocal将局部变量也加入到hash表中,并且对每个扫描到的局部变量都PUSH压入栈。
    所以在栈空间中调用函数时,最开始部分是原来调用函数的ARG,LOC寄存器值,接着是ARG参数部分,再后面是返回地址部分,最后是LOC局部变量部 分,ARG,LOC寄存器分别指向参数和局部变量第一个值的位置处。如可以通过arg(0)来引用第一个参数,loc(1)来引用第二个局部变量等。
    汇编代码生成后,最后就是run.c实现汇编代码的部分。在run.c中根据assemble.c中的代码相应的增加了ARG,LOC,ARGTMP寄存 器,并通过MEM_ARG_LOC_OP_GET及MEM_ARG_LOC_OP宏来操作全局变量以及函数里的参数和局部变量的内存部分。
    最后将FUNARG,RESET,RET汇编指令加以实现,就完成了FUN函数的实现。
   
    作者:zenglong
    时间:2012年3月17日
    官方网站:www.zengl.com

    因为时间和篇幅关系,请结合源代码中的注释,加上git工具以及vs2008或者eclipse+CDT或者gcc,gdb等工具进行分析。

    下面通过例子来说明fun函数定义和函数调用的实现。

    本节test.zl测试脚本中的代码如下(下面的注释是为了在这里进行说明而添加的,在源文件中并没有):

fun test(a,b)  //通过fun关键字定义一个名为test的函数,该函数接受两个参数:a和b ,这两个参数目前可以是数字或者字符串等。
  print 'your test function arg a is ' + a;  //打印第一个参数
  print 'your test function arg b is ' + b;  //打印第二个参数
endfun   //必须以endfun结束函数的定义

fun test2(a,b) 
//通过fun关键字定义一个名为test2的函数,该函数接受两个参数:a和b ,这两个参数目前可以是数字或者字符串等。
  print a;  //直接打印出第一个参数
  print b; 
//直接打印出第二个参数
endfun  //必须以endfun结束函数的定义

test('hello world','my name is zengl');   //调用test函数进行打印
test('welcome to zengl.com','my name is zengl.com');
test2('走自己的路,让别人去说吧','           ─────阿利盖利·但丁');   //调用test2函数进行参数的直接打印输出

    test.zl脚本经过main.c的词法扫描,parser.c的语法解析后,生成的AST(抽象语法树)如下:

 

    从上图可以看出fun关键字有三个子节点,第一个是函数名如test函数名,第二个是参数部分,这里有两个参数,所以是逗号作为子节点。第三个子节点是函 数执行体部分的第一条语句的顶级父节点,如本例的第一条print打印语句,函数体内其他的语句都是通过next node字段连接在一起的。
    函数调用部分,顶级父节点是函数名,子节点为参数部分。
    
    生成AST抽象语法树后,再经过symbol.c将扫描到的全局变量,局部变量,以及函数等符号信息存放到全局哈希表和对应的动态数组中,以后查找某个变量或者函数时,先在哈希表中查找动态数组中的索引,再由索引得到动态数组中的信息。
    生成符号信息后,再经过assemble.c生成基础汇编代码,此时的汇编代码中的函数地址还是funadr...之类的伪函数地址,最后由ld.c链接 器 将这些伪地址替换为真实的汇编代码位置。最终生成的汇编代码如下(以下的注释是在这里为了说明而添加的,在实际代码中并没有):

0 JMP 11;   //跳转到RET后面,这样在函数定义时就不会执行,只有在函数调用时才会执行函数里的代码。
1 FUNARG 2;  //该函数有两个参数,通过FUNARG告诉run虚拟机,当函数调用的实际参数不足两个时,就在堆栈中压入一些默认值,使得函数具有两个参数。
2 MOV AX "your test function arg a is ";  //将第一条print语句的加法左边的字符串赋值给AX虚拟寄存器
3 MOV BX arg(0);  //arg(0)以arg为前缀的表示第一个参数的栈内存空间(0表示第一个,1表示第二个,以此类推!)。
4 PLUS;   //完成加法运算,目前加法除了可以对数字进行运算外,还可以对字符串进行运算。
5 print AX;  //将PLUS加法运算后的结果通过print汇编指令打印显示出来。
6 MOV AX "your test function arg b is ";
7 MOV BX arg(1);  //第二个参数。
8 PLUS;
9 print AX; //完成第二条打印语句。
10 RET;   //从函数返回到调用该函数的位置继续执行,和汇编语言里的ret指令是一个意思。
11 JMP 16;   //第二个test2函数定义的开头,也是一个跳转,防止第一次定义时就执行代码。
12 FUNARG 2;  //指出函数的参数。
13 print arg(0);  //直接打印出第一个参数。
14 print arg(1);  //直接打印出第二个参数。
15 RET;           //返回。
16 PUSH ARG;  //在函数调用时,先将ARG寄存器(当前执行环境下的参数寄存器)压入栈中保存起来,当函数执行完后,RET返回时会将ARG寄存器进行恢复。
17 PUSH LOC;  //将当前环境下的LOC局部变量寄存器压入栈中。
18 RESET ARGTMP;  //通过RESET汇编指令将当前的栈顶地址赋值给ARGTMP(临时的参数寄存器,之所以要这个临时的寄存器是因为在下面生成参数的时候会用到当前环境的ARG寄存器,所以只有在参数生成完后,准备跳转时才将ARGTMP赋值给ARG寄存器)
19 MOV AX "hello world";  //将函数调用的第一个参数先赋值给AX寄存器。
20 PUSH AX;   //再将AX寄存器通过PUSH汇编指令压入虚拟堆栈中。
21 MOV AX "my name is zengl";   //将函数调用的第二个参数赋值给AX寄存器。
22 PUSH AX;    //再将AX寄存器通过PUSH压入虚拟堆栈中。
23 PUSH 27;   //将返回地址压入栈中。当函数调用执行完后,在函数RET指令执行时就会根据这个返回地址跳转到该位置继续执行。
24 MOV ARG ARGTMP;  //将ARGTMP临时参数寄存器赋值给ARG寄存器。
25 RESET LOC;  //RESET设置LOC寄存器的值为当前堆栈的栈顶位置,堆栈有个计数器用于记录当前的栈顶位置,下次PUSH时就会压入到栈顶位置,因为随后在函数里会将局部变量压入栈,所以此时的栈顶位置可以代表LOC局部变量的起始位置。当然如果函数调用的实参不足时,在函数FUNARG执行时会多压入一些参数,所以LOC寄存器的值在FUNARG时还有可能会修改。
26 JMP 1;      //这里的1是函数test的汇编代码位置,通过JMP 1 跳转到test函数,从而执行test函数里的代码。
27 PUSH ARG;  //以下是第二个test函数调用的实现,原理同上。
28 PUSH LOC;
29 RESET ARGTMP;
30 MOV AX "welcome to zengl.com";
31 PUSH AX;
32 MOV AX "my name is zengl.com";
33 PUSH AX;
34 PUSH 38;
35 MOV ARG ARGTMP;
36 RESET LOC;
37 JMP 1;
38 PUSH ARG;   //以下是test2函数调用的汇编实现,原理同上。
39 PUSH LOC;
40 RESET ARGTMP;
41 MOV AX "走自己的路,让别人去说吧";   //第一个参数。参数可以是数字或字符串。
42 PUSH AX;
43 MOV AX "           ─────阿利盖利·但丁";   //第二个参数。
44 PUSH AX;
45 PUSH 49;
46 MOV ARG ARGTMP;
47 RESET LOC;
48 JMP 12;   //跳转到test2函数的汇编代码位置来执行test2函数。
49 END;
 

以上是该例子生成的test.zlc汇编代码文件部分。从这里可以看出函数和函数调用的实现原理(一般原理或者真理性质的东东都是在汇编或者脚本语言生成的中间代码中可以看到)。

对于函数,比较难理解的地方就是函数调用时的虚拟栈结构(虚拟栈<简称栈>和虚拟内存都是run.c实现的虚拟机在运行时创建的动态数组空间。) ,一个函数调用的栈存储情况如下:

 

    上图为函数调用时的栈空间的情况,在函数调用跳转到函数内部后,当前的ARG寄存器就指向图中的压入的参数区域的顶部,即指向第一个参数,LOC寄存器就指向图中的函数局部变量的顶部,即指向第一个局部变量。

    通过上面的介绍,应该对zengl脚本语言的函数原理比较清楚了,其实汇编语言里在函数调用时的栈结构也差不了多少,都是将参数,局部变量和返回地址以及原来调用环境下的寄存器值都保存在栈中,在函数调用返回时,再将这些值进行恢复。

    其他更多的细节部分,请结合源代码,git log -p 和前面提到过的开发调试工具进行分析。

    最后是些老生常谈的话题:
    windowsXP压缩包中的代码包括test.zl测试脚本都是采用GBK的编码,Linux压缩包中的代码包括测试文件以及git里的信息都是UTF8的编码,所以如果哪些地方出现了乱码,请自行调整。

    对于windows用户,请确保在项目属性的配置里,命令行参数配置的是test.zl(对于zengl_lang_v0.0.12的项目)或test.zlc(对于zenglrun的项目),好像每一节都提到过。
    另外对于vs2008的用户,我在项目属性里:[配置属性>>>>C/C++ >>>> 高级] 部分设置了禁用特定警告:4013,4715,4996 ,这几个警告会显示一些某某函数是非安全的函数,或者函数没有返回值等,这里禁用掉,防止出现过多的警告。另外还有个警告是显示某某变量没被使用过的, 这个警告我没禁用,可以不用管它。我最开始是使用Linux系统开发的zengl ,在我的GCC下面并没有显示过这些讨厌的警告,所以就没处理,不过还好这些警告都无关痛痒,无需理会。

    linux系统下的用户请结合usage.txt的说明,先运行make clean 将原来生成的zengl zenglrun 和 main.o parser.o assemble.o ld.o func.o run.o  symbol.o文件删除。

    再运行make all (单纯的make只能生成zengl,所以需要make all来生成所有的目标)

    生成zengl zenglrun 和 main.o parser.o assemble.o ld.o func.o run.o  symbol.o。(在生成过程中如果出现一些警告,暂不管他)

    最后运行 ./zengl test.zl 查看printASTnodes函数打印抽象语法树节点的结果,以及符号表输出的变量信息以及本节新增的函数信息等。(例如变量的内存地址,以及在源文件的行列号,函数的唯一标识ID等)。
    接着运行./zenglrun test.zlc (注意是.zlc结尾的文件名,因为zenglrun虚拟机只能运行.zlc里的汇编代码)。

    本节涉及到的很多高级的编译原理都可以在《龙书》中找到。

    最后的最后,如果转载请注明来源 http://www.zengl.com   , OK , 先到这里,休息,休息一下 O(∩_∩)O~

上下篇

下一篇: zengl编程语言v0.0.13函数中实现global和return

上一篇: zengl编程语言v0.0.11实现循环控制结构

相关文章

zengl v1.8.0 缓存内存中的编译数据,跳过编译过程

zengl v1.9.0 增加self特殊类名,增加zenglApi_SetDefLookupHandle等接口

zengl v1.7.4 修复Bug

开发自己的编程语言-zengl编程语言v0.0.2初始化抽象语法树

zengl编程语言v1.0.5 编译,执行,源码调试一体化,开发俄罗斯方块

zengl编程语言v0.0.13函数中实现global和return