Lua源码阅读:命令的解析与执行
<p>具体分析文档如何被解析、生成对应命令以及到虚拟机执行的流程。</p>
Lua词法:
Lua使用一遍扫描代码文档的方式生成字节码,即在第一遍扫描代码的时候就生成字节码了,这么做主要是为了加快解释执行的速度。
赋值类命令:
局部变量:
lua代码:
1 | local a = 10 |
相关的EBNF词法:
1 | chunk -> { stat [`;'] } |
第3行为赋值,涉及几个问题:
- = 左边是一个变量,只有变量才能赋值,于是涉及以下问题:如何存储局部变量,如何查找变量,怎么确定一个变量是局部变量、全局变量还是UpValue?
- = 右边是一个表达式列表 explist1,在这个最简单的例子中,这个表达式是常量数字 10。这种情况很简单,如果不是一个立即能得到的值,比如是一个函数调用的返回值,或者是对这个 block 之外的其他变量的引用,又怎么处理呢?
第一个问题,如何识别局部变量?
首先在函数 localstat 中,会有一个循环调用函数 new_localvar,将=左边的所有以逗号分隔的变量都生成一个相应的局部变量。
存储每个局部变量的信息时,我们使用的是 LocVar 结构体:
1 |
|
其中变量名放在 LocVar 结构体的变量 varname 中。函数中所有局部变量的 LocVar 信息,一般存放在 Proto 结构体的 LocVar 中。
在结构体FuncState 中,成员变量 freereg 存放的就是当前函数栈的下一个可用位置。
在 Lua5.3 版本中,我找到了一个类似的函数:
1 | // lparser.c |
这里调用 statement()函数:
1 | static void statement (LexState *ls) { |
可以看到在第 9 行对当前函数栈存放的变量数量(包括函数的局部变量、函数的参数等)进行调整。
那么,nactvar 这个变量又是何时调整的呢?在这个例子中,变量 a 是一个局部变量,最后会在解析局部变量的函数 adjustlocalvars 中进行调整:
1 | static void adjustlocalvars (LexState *ls, int nvars) { |
至此,第一个问题得到了解决:在函数 localstat 中,会读取=号左边的所有变量,并在 Proto 结构体中创建相应的局部变量信息;而变量在 Lua 函数栈中的存储位置存放在 freereg 变量中,它会根据当前函数栈中变量的数量进行调整。
第二个问题:表达式的结果如何存储?
解析表达式的结果会存储在一个临时数据结构 expdesc 中:
1 | typedef struct expdesc { |
- 变量 k 表示具体的类型
- 后面紧跟的 union u 根据不同的类型存储的数据有所区分,具体可以看 expkind 类型定义后面的注释
- 至于 t 和 f 这两个变量,目前可以暂时不管,这是跳转相关的命令。
解析表达式列表:
解析表达式列表的函数为explist:
1 | static int explist (LexState *ls, expdesc *v) { |
- 调用函数expr解析表达式
- 当解析的表达式列表中还存在其他的表达式时,即有逗号(,)分隔的式子时,针对每个表达式继续调用expr函数解析表达式,将结果缓存在expdesc结构体中,然后调用函数 luaK_exp2nextreg 将表达式存入当前函数的下一个可用寄存器中。
根据上面的调用路径,最终会走入 simpleexp 中:
1 | static void simpleexp (LexState *ls, expdesc *v) { |
- 使用类型 VKINT 初始化 expdesc 结构体,这个类型表示数字常量
- 将具体的数据赋值给 expdesc 结构体中的 ival,前面说过,expdesc 结构体中 union u 的数据根据不同的类型会存储不同的信息,在 VKINT 这个类型下就是用来存储数字的。
现在这个表达式的信息已经存放在 expdesc 结构体中,需要进一步根据这个结构体的信息来生成对应的字节码。
这个工作由函数 luaK_exp2nextreg 完成,需要根据 expdesc 结构体生成字节码时,都要经过它:
- 调用 luaK_dischargevars 函数,根据变量所在的不同作用域(local,global,upvalue)来决定这个变量是否需要重定向
- 调用 luaK_reserveregs 函数,分配可用的函数寄存器空间,得到这个空间对应的寄存器索引。有了空间,才能存储变量
- 调用exp2reg 函数,真正完成把表达式的数据放入寄存器空间的工作。在这个函数中,最终又会调用dischargereg函数,这个函数式根据不同的表达式类型(NIL,布尔表达式,数字等)来生成访问表达式的值到寄存器的字节码
在函数 discharge2reg 中最终会走到这里:
1 | static void discharge2reg (FuncState *fs, expdesc *e, int reg) { |
在这个函数的参数中,reg 参数就是前面得到的寄存器索引,于是最后根据 k 值生成了OP_LOADK命令(另一种情况是生成OP_LOADKX命令),将数字 10 加载到 reg 参数对应的寄存器中。
至此,通过对这个最简单的向局部变量赋值操作的分析,完成了 Lua 解释器从词法分析到生成字节码的全过程分析:
- 每个局部变量都有一个对应的 LocVar 结构体存储它的变量名信息。
- 每个局部变量都会对应分配一个函数栈的位置来保存它的数据。
- 解析表达式的结果会存在 expdesc 结构体中。根据不同的类型,内部使用的联合体存放的数据有不同的意义。
- luaK_exp2nextreg 是一个非常重要的函数,它用于将 expdesc 结构体的信息中存储的表达式信息转换成对应的 opcode。
流程图:
如果代码变为:
1 | local a,b = 10 |
则在localstat函数中进入:
1 | static void localstat (LexState *ls) { |
第一个函数adjust_assign用于根据等号两边变量和表达式的数量来调整赋值。具体来说,在上面这个例子中,当变量数量多于等号右边的表达式数量时,会将多余的变量置为NIL。
第二个函数adjustlocalvars会根据变量的数量调整FuncState结构体中记录局部变量数量的nactvar对象,并记录这些局部变量的startpc值。
如果代码变为:
1 | local a = 10 |
主要区别在于走到simpleexp函数时,进入的是另一条路径,走入了primaryexp函数中。然后在prefixexp函数中,判断这是一个变量时,会调用singlevar函数(实际上,最后会调用递归函数 singlevaraux)来进行变量的查找,这个函数的大体流程如下:
- 如果变量在当前函数的 LocVar 结构体数组中找到,那么这个变量就是局部变量,类型为 VLOCAL。
- 如果在当前函数中找不到,就逐层往上面的 block 来查找,如果在某一层查到了,那么这个变量就是 UpValue,类型为 VUPVAL。
- 如果最后那层都没有查到,那么这个变量就是全局变量,类型为 VGLOBAL。
在 luaK_dischargevars 函数中,根据三种不同的类型有不同的操作。
如果赋值的源数据是局部变量,则使用 MOVE 命令来完成赋值。
全局变量:
1 | a = 10 |
这时对应的 luaK_dischargevars 函数这样:
1 | void luaK_dischargevars (FuncState *fs, expdesc *e) { |
而在 Lua 5.3版本中,应该是在这部分函数中对全局变量进行处理的(存疑)。
1 | static void singlevar (LexState *ls, expdesc *var) { |
参考: