关闭

Lua 源码分析之一切从这里开始

851人阅读 评论(0) 收藏 举报
分类:

         Lua是边进行语法分析,边词法分析。其中,词法分析的模块是:llex。其对外暴露的接口是:llex_next()。并且,在整个语法分析、词法分析的过程中,只有一个唯一的全局实例:llex_state。

         在词法分析的过程中,lua会处理以下几种情况:

<1> ‘/r’, ‘/n’ 这两个东西的任意组合,是换行,lua直接跳过,其中是用inclinenumber(ls)这个函数跳过的。

<2> -- 或 –--[ ] 注释,lua也会跳过,不过,lua是通过把注释当作字符串读取后跳过的。

<3>[==[  ]==],中间是等量=的字符串,lua会读取字符串,通过read_long_string()。并返回:TK_STRING。

<4> “ ”,‘ ’ 用引号引起来的字符串,lua会通过read_string()读取字符串。并返回:TK_STRING。

<5> ==,等于号,这是lua的保留符号,返回:TK_EQ。

<6> <,小于号,返回:TK_LT。

<7> >,大于号,返回:TK_GT。

<8> ~,返回~,如果下一个字符是=,如下:

<9> ~=,不等于号,返回:TK_NE。

<10> . , 返回:’.’。

<11> .. , 返回:TK_DOTS。

<12> … , 返回:TK_CONCAT。

<13> .(0~9) , 返回:TK_NUMBER。

<14> EOZ , 返回:TK_EOS。

<15> 如果是数字开头,则lua会读取数字,用函数read_numberal(),返回:TK_NUMBER。

<16> 如果是字母开头或_开头,则为变量、函数名或保留字,lua会区别开来,分别返回保留字的标志或TK_NAME。

<17> 如果是:+-*/等,lua会返回其ASCII码。

 

注:

1、  如果遇到[=,lua会假设这是一个长字符串,并检查其后面的=的数目,并且如果=后面没有紧跟着[,则报错。

2、 有时候数字是小数,直接以.开始,如:.31415926。lua会把这个当作一个数字,也就是TK_NUMBER类型。

3、 因为在不同国家,小数点可能不同,lua会检查到这种情况,并作处理。

4、 以下划线_开头的文字,lua会当作保留字处理。

5、 当lua读取一个字符串的时候,会新建一个TString类型的数据,并存在G(L)中,以便进行垃圾回收。

6、 保留字是lua预先建好的TString类型的字符串,在lua初始化的时候存在了G(L)中。

7、 在lua中,字符串是常量,所以,当在程序中用到相同的字符串的时候,lua会让他们指向同一个G(L)中的字符串,以便节省内存。

(2008-10-30)

 

 

第1章        lua是如何生成中间码的

lua会解释lua程序,然后生成中间码。生成中间码的过程是和语法解析一起的。而且

整个是由语法解析主导的。具体来说,语法解析程序执行,这个语法解析程序是luaY_parser(),遇到一个词(token),就会调用词法解析程序:llex_next(),然后分析这个token,进入合适的语法解析函数,例如:如果 token是if,那么就会进入ifstat()函数进行解析,如果token是while,就会进入whilestat()函数进行解析。这个期间会进行语法检查,如果出现错误语法,lua便会退出,并报错。在解析的过程中,lua会同时生成中间码。在整个过程中,词法分析伴随左右,并且,生成的字节码也是保存在词法分析对象里面,就是那个唯一的全局对象:llex_state,简称ls。 ls里面有一个fs,就是FuncState,代表当前函数(或者代表整个lua程序,还有待研究),里面有个ls->fs->f->code[]数组,lua生成的中间码就保存在这个里面。在语法解析的过程中,有个指示当前生成代码位置的指针,叫pc,在ls->fs->pc中。这个pc是递增的,在整个过程中都不会减,所以,可以得知,lua代码生成是顺序的,它解析到一个地方,就会生成一个代码,不会跳着生成。

 

第一节 定义局部变量

         局部变量和全局变量是程序的基础,没有变量如何写程序?

         首先看局部变量,当lua解析程序,碰到local保留字的时候,就会了解到后面的程序是在定义一个局部变量或者局部函数。首先,如果是定义局部变量的话,又分为以下几种情况:

1、  local a;                                  // 没有=的情况

2、 local a = 1;                          // 左右两端的参数个数相同

3、 local a = 1,2;                      // 右端的值个数多于左边变量个数

4、 local a , b = 1;                    // 左端变量个数多于右边变量个数

 

         首先,lua会跳过local这个保留字,用函数llex_next()跳过。然后,lua会测试下一个 token是不是function保留字,也就是说,是不是在定义一个局部函数,如果是,就用localfunc()函数进行解析,否则,是在定义一个局部变量,用localstat()函数进行解析。

        

         对于解析函数,后面再看,先看localstat()是如何解析的。

 

         首先,根据语法,local 后面要跟一个变量名。这时,词法解析要返回的是TK_NAME。这个变量名不能与保留字相同,不然词法解析会返回保留字,而不是TK_NAME。Lua会通过str_checkname()进行检查。如果检查不通过,lua会退出并报错。上文已经说过,lua进行词法解析的时候,遇到字符串(其中变量名是一个字符串,保留字也是),lua会在G(L)中创建一个TString类型的对象,并在全局用到这个字符串。

         在我们的例子local a;中,lua碰到了a,就会在G(L)中创建一个字符串”a”,然后,在localstat()函数里,会用到一个指向这个”a”字符串的指针。

         当词法分析注册了这个局部变量名字符串后,localstat()函数就会创建一个局部变量,其中是通过函数new_localvar()创建局部变量的。这个函数做了什么事呢?首先,它会检查一个函数的局部变量个数是否已经最大,在lua中,定义了一个宏LUAI_MAXVARS,是200,也就是说lua中一个函数所能容纳的局部变量个数是200个。如果局部变量的个数已经达到了200个,此时再向里面注册局部变量,lua就会报错,通过函数errorlimit()函数报错。

         当局部变量个数检测也通过了,lua就可以注册这个新的局部变量了。前面说的ls->fs,这里面有一个数组ls->fs->actval[]。然而,真正的局部变量是通过函数registerlocalval()注册的,这个函数很简单,前面说过,在ls->fs里,有一个函数头ls->fs->f,registerlocalval()会把局部变量注册到这个ls->fs->f中去,其中,ls->fs->f->locvar[]数组是用来保存这些局部变量的。而,ls->fs->actval[]里面保存的只是这个局部变量在ls->fs->f->locvar[]里的位置。

         Lua会遍历local 后面的TK_NAME类型的token,并记录下来有多少了局部变量,记录在nvals里面。

         在例子:local a;里面,nvals 就是1;

         在例子:local a, b;里面,nvals 就是2;

在这个遍历的过程中,分割token的是逗号,,如果是其他符号,lua会检查是不是等号=,如果是=,那么变量部分就算遍历完了,如果是其他符号,lua就会假设局部变量定义结束。

         这里说一下,lua的语法解析过程是寄存器式的,也就是,所有的变量都存在寄存器里面。这里的寄存器不是汇编里的寄存器。实际上,在lua虚拟机里面有一个栈,而栈的每一个槽就是这里所说的寄存器。而寄存器与栈的区别就是,栈里存取不是随机的,要严格按照先进后出的原则进行,而用了寄存器的概念,通过寄存器可以随时取栈里的每一项,其实,这里不称为栈,称为数组更合适。不过想一下intel函数的执行过程,函数的局部变量是存在栈里面的,但是在函数内是通过ebp + offset存取的,和这里的方法一样。

         当lua遍历局部变量,并注册,这里的注册已经说过,不过要指出的是,registerlocalval()只是把当前局部变量的名字记录了下来,同时记录了这个局部变量有效期,也就是从哪条指令开始有效,当然,是从局部变量定义结束那条指令开始有效,但是,并没有储存局部变量的值,实际上,局部变量的值是存在了栈上,就像intel里的函数一样,开始一个函数的时候,会在栈上开辟一段空间,储存局部变量,在这里,局部变量也是储存在栈上,至于在栈的哪个位置,这就是靠默认在当前第一个空寄存器上对应着这个局部变量。这个具体细节待会再讲。

         现在讲到了,lua遍历完了局部变量,后面就有两种情况,也就是后面有没有等号=。如果有等号,lua会假设=后面就是要赋值的值,下面就开始处理=后面要赋的值了。是通过explist1()进行的。现在再想一下,后面的值可能是多种多样的,可能是一个函数的返回值,可能是一个全局变量,一个常数,一个字符串,一个table,一个初始化table的式子,或者是其他的局部变量等等。这么多情况,我们不可能在这里就分析完整。对于读取一个式子,在后面解析,现在只假设后面是一个常数,或者一个字符串,或者一个全局变量,或者一个局部变量,或者是闭包里的一个值。也就是说,我们假设后面不是一个式子,也不是一个函数调用。

         这里,就到了进行参数解析的explist1()里面了。在这里,不得不提的一个函数是expr(),这是读取一个表达式的函数,我们知道,在c里面,一个变量,或者数学式,或者函数等等,只要是以分号;结束的式子,都是表达式,在lua里也一样,expr()就是读取这些式子的一个函数,功能非常强大,是通过递归来解析的。对于数学式,也就是说形如:a+b, a/b, a*b等等的式子,或者NOT a,a AND b等等逻辑式子,这样的表达式是复杂表达式,在lua中有一个叫simpleexp()的函数,它是分析简单表达式的,也就是说没有操作符的式子,比如:常数,变量,字符串,函数等等。所以,我们这里就不管复杂表达式了,只管简单表达式,而且,也不管函数。这些后面再议论。

         下面,就把我们要讨论的给局部变量赋值的值分为以下几类:

1、  常数、字符串:这统称为常量。Lua中会专门对待常量,在ls->fs里面,有一个数组,专门存放在这个数组里面出现的常量。这里要说一个东西,常量是有名字的,可以显式的给它命名,比如c里面的const char val[] = “abc”;,就是命了名的常量,不过,常量也可以不显式命名的,比如我们的定义局部变量:local a = “abc”;这里的字符串“abc”就是一个常量,它在内部有个名字,就叫做“abc”。根据常量的名字,lua中广泛运用了哈西表,根据常量的名字,ls->fs->h是一个哈西表,通过求哈西值,在此表中对应有一项,而这项里面存的是这个常量在常量数组里的位置,也就是在ls->fs->k[]这个常量数组里的位置,这样,通过哈西表可以快速定位一个常量。

2、 NIL,FALSE,TRUE这三个特殊的值。

3、 局部变量和全局变量,也就是说从一个变量给另一个变量赋值。

4、 闭包里的变量。也就是说,这个变量不在本函数内,但也不是全局的变量,而是其上层函数里的局部变量,或者上上层,上上层……

 

         接下来,就对以上4种情况,分析lua是怎么给局部变量赋值的。这里,用到了一个结构体,expdesc。这是一个用来描述表达式的结构体。下面会一直用到。

         首先,当lua用simpleexp()分析等号右边的值的时候,会传一个指向expdesc结构体的一个对象的指针expdesc*v,用分析的结果填充这个结构体。当遇到当前token的类型是TK_NUMBER的时候,就会初始化这个结构体,将这个表达式结构体的类型v->k赋值为VKNUM,并在这个结构体中记录下这个TK_NUMBER的值:v->u.nval = ls->t.seminfo.r。其中ls->t.seminfo.r就存着lua词法分析出来的TK_NUMBER类型的数值。

         如果遇到的token的类型是TK_STRING,lua会根据上面讲的处理常量的方法,在fs->h这个哈西表里记录这个字符串在常量数组中的位置,并将v的类型v->k赋值为VK,代表一个常量,然后在v->u.s.info中记录这个字符串常量在f->k[]这个常量数组里的位置,也就是和在fs->h 这个哈西表里存的值相同。

         如果遇到的token类型是TK_NIL,就将v->赋值为VNIL。

         如果遇到的token类型是TK_FALSE,就将v->k赋值为VFALSE。

         如果遇到的token类型是TK_TRUE,就将v->k赋值为VTRUE。

         对于TK_NAME这种情况,simpleexp()会交给primaryexp()函数去处理。

 

         这里讲的primaryexp(),处理的是一个比较复杂的式子,类似于下列式子:

1、  fatherObj.childObj.array[fieldname].func({table construction});

2、 fatherObj.childObj.array[fieldname]:func({table construction});

3、 fatherObj.childObj.array[fieldname]:func{table construction};

4、 fatherObj.childObj.array[fieldname].func{table construction};

         其实这些式子都一样,不过仔细看一下有一些少许的差别,相信大家对lua比较熟悉的话肯定一眼就可以看出来为什么分了这4组。

         当然,这么复杂的式子我们暂不研究,只研究如下的形式:

1、  fatherObj;

         也就是说,不带点.,不带框[],不带冒号:,不带大括号{},只有一个变量的形式。

         这个时候,primaryexp()又把这么简单的任务交给了prefixexp()这个函数取处理。我们来看一下prefixexp()是怎么解析的。

         其实prefixexp()也不是只做这么简单的事情的,他还考虑到如果有小括号怎么办。也就是:

(fatherObj.childObj.array[fieldname].func({table construction{}}))

这种情况。

         我们都知道,用小括号括起来的东西,可以看成一个整体,prefixexp()就是研究这个整体的东西。对于真正的一个变量的形式,prefixexp()还是交给了singleval()这个函数去做。

         也就是,对于token类型是TK_NAME的情况,最终是由singleval()这个函数去做的。

         首先,singleval()要检测一下,这个标为TK_NAME对应的东西是不是真的是一个标为TK_NAME的东西,也就是说,虽然别人会把一个标为TK_NAME的东西传给它,它也要防止别人把不是TK_NAME的东西传给它,还是用str_checkname()这个函数去检测,检测不通过lua同样是推出。

         然后,虽然确定了这是一个变量,但还不能确定这是一个全局变量、局部变量还是一个闭包变量(upvalue)。这个确定的操作,就是singlevalaux()函数去做。

         对于局部变量来说,很简单,只是查一下本函数的局部变量表,就是前面说的fs->f->locvar[]数组,如果找到了,就会给v->k赋值为VLOCAL,并将v->u.s.info赋值为此局部变量在此局部变量表里的位置。

         对于非局部变量,就有2种情况了。第一种,就是全局变量;第二种就是闭包变量。因为这两种情况下,变量都不在本层函数内,这样,就要逐个搜索上层函数,如果在某上层函数里发向这个变量是这个上层函数的局部变量,就是闭包变量,否则,检查到了最外层的函数的时候,还没发现有哪一层函数的局部变量里包含此变量,就将其当成全局变量来处理。

         所以,总的来说,全局变量也是比较简单的,递归检查上层函数,如果没有发现此变量的定义,就把这个变量当成全局变量,并将v->k赋值为VGLOBAL,并将v->u.s.info赋值未NO_REG,也就是说这个全局变量还没有对应寄存器,这个东西后面再说是在做什么。

         下面一种情况就相对比较复杂,也就是闭包变量的情况。当即不是局部变量,又不是全局变量的时候,也就是说在某一上层函数里面,找到了此变量的定义,这是要做这么一件事情,就是把这一上层函数标记一下,说明这里面有供后面函数用的闭包变量,具体就是:fs->bl->upval=1。其实这一句包含了好多东西,我省略成了这一句。具体来说就是这样的,每个函数里都有好多块,例如: if块,while块,for块等等(block,也就是上面的bl, fs->bl)。这个变量不是定义在函数里的,而是定义在一个块里的,所以要将一个块标记成它具有供后面函数用的闭包变量,而不是将函数标记。而在一个函数里寻找这个块也需要一些代码,虽然很简单,但是要说清楚还是要花上一些语言,所以先省略。这里只了解,如果后面函数引用了前面某个函数的变量,就会把前面这个函数中的这个变量定义的那个块标记成有闭包变量。

         接着说,如果发现是闭包变量(upvalue),也就是说,在某一上层中发现了这个变量的定义,因为这是个递归查找的过程,此时,就要递归回去了,在每经过的一层,要在这一层里记录下这个变量,即在当前函数层fs->upvalues[]数组里查找这一项,如果没有记录过,就在里面记录一项,其中记录两个元素,第一,记录这个闭包变量在上一层函数里是局部变量呢还是闭包变量;第二,记录这个闭包变量在上一层函数闭包变量数组里或局部变量数组里的位置。这样层层递归回来,就在经过的每一层记录了这个闭包变量。

        

         于是,上面就说明了lua解析一个变量名的过程,很简单。结论就是,将这个变量的位置确定了下来,或者是局部变量,或者是全局变量,或者是闭包变量,并在v里面记录下来了这个变量的信息。

         好了,现在补充一下,为什么当解析一个Global变量的时候,会把v->u.s.info赋值为NO_REG,因为现在要更正,其实当初赋值为任何东西都行。

         怎么更正呢?具体说来,就是,把这个全局变量的变量名存在fs->k[]常量表里面,并把v->u.s.info改为此常量数组的第几项。

         现在回到explist1()里面。explist1()会读取每一个遇到的token,如果是常量,NIL,TRUE,FALSE等,很简单,就是在 v里标记一下,如果是遇到了 TK_NAME,就会按上面的方法,把这个变量名确定下来,同时也在v里标记下来。注意,这个explist是一个以逗号隔开的值,explist1()每处理一个token,就会检查后面是不是一个逗号,如果是的话,就会把这个token处理掉,然后读取下一个token。这里所说的处理掉就是指生成中间码。具体是通过luaK_exp2nextreg()函数调用处理的。这个处理过程待会就讲,现在需要注意的一个地方是,最后一个token是没有处理的,只有当后面有逗号的时候,前一个token才会被处理。

         好了,现在就开始将如何通过luaK_exp2nextreg()来处理这个token的。

         这个时候,我们首先看,对于局部变量、闭包变量和全局变量的处理方式,对于常量,后面再讲。

         如果是局部变量的话,luaK_exp2nextreg()会通过luaK_dischargevars(),将v->k由VLOCAL改成VNONRELOC,也就是说,这个局部变量所在的位置是不可以移动的。这个好理解,想想intel调用函数的时候,是将函数的局部变量存在了栈的一个特定位置,通过ebp+offset来使用,局部变量在栈上的位置是不可以别移动的。Lua和这个一致。

         如果是全局变量的话,lua会立即生成一条中间码(Opcode):通过函数luaK_codeABx(),其中这条中间码的操作符(Op)是OP_GETGLOBAL。并且v的v->k改为VRELOCABLE,也就是说这个全局变量可以放在栈的任意位置,只要能访问到。

         如果是闭包变量的话,lua也会立即生成一条中间码,其中操作符是:OP_GETUPVAL,并且v->k也改成VRELOCABLE。并把v->u.s.info也编进指令中去,代表是从upval[]数组的第几项取值。

         上面这个操作完成后,都将生成的指令在指令数组ls->fs->f->code[]里的位置记录在v->u.s.info中。

         这里要再说明一下,虽然上面生成了中间码,比如OP_GETGLOBAL,但是,当lua的虚拟机运行到这里的时候,还是无法正常执行的,因为虽然操作符知道了,是OP_GETGLOBAL,将其放在当前函数运行栈的哪个位置呢?这里并没有说明,所以,后面会有对其修复的地方,这到后面再提。

         而且,对于VLOCAL变量而言,并没有生成中间码,所以,后面肯定还会进行修复。

         进行过上面的操作后,lua在栈上保留一个寄存器槽,也就是ls->freereg++;接着通过exp2reg()函数,将上面提到的指令中几块空的地方补全。

         具体操作如下:

1、  对于闭包变量(Upvalue)和全局变量,因为上面已经生成了中间码,这里就不需要再生成了,不过就是要将值放到栈的哪个位置没确定,而且,v->k是VRELOCABLE,此时,就要把寄存器补上。具体是:根据v->u.s.info里存的指令的位置取出此指令,并将当前第一个空的寄存器位置补充在里面,代表将这些变量读取并放在栈里,以便使用。实际上,因为栈里存的都是局部变量,这里将这些变量读取并给栈里的空槽赋值,其实就是对要初始化的局部变量赋值了。

2、  对于局部变量,需要说明一下,如果是用自己给自己赋值,也就是形如一下的语句:local a = a,这时候,lua是不做任何事情的,否则,就会生成一条OP_MOVE指令,用来用以前的局部变量给新的局部变量赋值了。

 

到此为止,关于闭包变量(upvalue)、全局变量和局部变量的处理就完成了。

 

         也就是,关于赋值符号=右边的值的处理及赋值都已经完成了。不过还要有些善后的动作。

 

         有一个地方,也就是,等号左右两边的数目不等。如果是变量的个数多于要赋值的个数,多于的变量要赋值为nil,如果是值多的话,多余的值就要丢弃掉。这些操作,其中变量个数多于要赋值的个数这种情况就是让adjust_assign()执行的。

         这里有一个要注意的,前面也说过,最后一个值不做处理。因为在这里要处理。更深入的原因是:如果定义的局部变量的个数比值得个数多,那么,最后一个值如果返回多个值的话,也就是通过函数调用等,要进行最后的调整(这点还没仔细研究)。

         如果最后一个值是一个变量、常量,也就是在我们现在的分析范围之内。Lua会简单得按照上面说的方法处理。然后,如果是要定义的变量多于要赋的值,就在栈中为多余的值保留位置,并生成OP_LOADNIL的指令,为每一个多余值的栈中的槽赋值为nil。在生成OP_LOADNIL指令的时候,有一些小技巧,进行优化,在luaK_nil()中进行,这里就不再深入讨论了。

         如果值多于要赋值的变量的话,lua会采取一种非常简单的方法,首先,按照上面的方法,对于每一个值都生成一个中间码,不管有没有要赋值的变量与之对应,到了最后,因为要初始化的变量个数是可以知道的,存在了ls->fs->nactvar中,此时,只要将下一个空闲寄存器的位置指定为在栈的ls->fs->nactvar位置上,也就是在全部局部变量后面,这样,虽然多生成了中间码,不过这些中间码执行的操作会被覆盖掉。

 

         到此为止,局部变量的赋值操作就结束了。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:525659次
    • 积分:6908
    • 等级:
    • 排名:第3317名
    • 原创:7篇
    • 转载:315篇
    • 译文:0篇
    • 评论:58条
    最新评论