具体分析文件如何被解析、生成对应指令以及到虚拟机执行的流程。

Lua词法:

Lua使用一遍扫描代码文件的方式生成字节码,即在第一遍扫描代码的时候就生成字节码了,这么做主要是为了加快解释执行的速度。

赋值类指令:

局部变量:

lua代码:

1
local a = 10

相关的EBNF词法:

1
2
3
4
5
6
7
chunk -> { stat [`;'] }
stat -> localstat
localstat -> LOCAL NAME {`,' NAME}[`=' explist1]
explist1 -> expr {`,' expr}
exp -> subexpr
subexpr -> simpleexp
simpleexp -> NUMBER

第3行为赋值,涉及几个问题:

  • = 左边是一个变量,只有变量才能赋值,于是涉及以下问题:如何存储局部变量,如何查找变量,怎么确定一个变量是局部变量、全局变量还是UpValue?
  • = 右边是一个表达式列表 explist1,在这个最简单的例子中,这个表达式是常量数字 10。这种情况很简单,如果不是一个立即能得到的值,比如是一个函数调用的返回值,或者是对这个 block 之外的其他变量的引用,又怎么处理呢?

第一个问题,如何识别局部变量?

首先在函数 localstat 中,会有一个循环调用函数 new_localvar,将=左边的所有以逗号分隔的变量都生成一个相应的局部变量。

存储每个局部变量的信息时,我们使用的是 LocVar 结构体:

1
2
3
4
5
6
7
8
9
10
// lObject.h
/*
** Description of a local variable for function prototypes
** (used for debug information)
*/
typedef struct LocVar {
TString *varname;
int startpc; /* first point where variable is active */
int endpc; /* first point where variable is dead */
} LocVar;

其中变量名放在 LocVar 结构体的变量 varname 中。函数中所有局部变量的 LocVar 信息,一般存放在 Proto 结构体的 LocVar 中。

在结构体FuncState 中,成员变量 freereg 存放的就是当前函数栈的下一个可用位置。

在每一个 chunk 函数中,都会根据当前函数栈存放的变量数量(包括函数的局部变量、函数的参数)进行调整(该书基于 lua 5.14版本写成)

1
2
3
4
5
6
7
8
9
// lparser.c
static void chunk (LexState *ls)
{
/* chunk -> { stat [`;']} */
while (!islast && !block_follow(ls->t.token))
{
ls->fs->freereg = ls->fs->nactvar; /* free registers */
}
}

在 Lua5.3 版本中,我找到了一个类似的函数:

1
2
3
4
5
6
7
8
9
10
11
// lparser.c
static void statlist (LexState *ls) {
/* statlist -> { stat [';'] } */
while (!block_follow(ls, 1)) {
if (ls->t.token == TK_RETURN) {
statement(ls);
return; /* 'return' must be last statement */
}
statement(ls);
}
}

这里调用 statement()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void statement (LexState *ls) {
int line = ls->linenumber; /* may be needed for error messages */
enterlevel(ls);
switch (ls->t.token) {
case TK_LOCAL: { /* stat -> localstat */
luaX_next(ls); /* skip LOCAL */
if (testnext(ls, TK_FUNCTION)) /* local function? */
localfunc(ls);
else
localstat(ls);
break;
}
}
lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg &&
ls->fs->freereg >= ls->fs->nactvar);
ls->fs->freereg = ls->fs->nactvar; /* free registers */
leavelevel(ls);
}

可以看到在第 9 行对当前函数栈存放的变量数量(包括函数的局部变量、函数的参数等)进行调整。

那么,nactvar 这个变量又是何时调整的呢?在这个例子中,变量 a 是一个局部变量,最后会在解析局部变量的函数 adjustlocalvars 中进行调整:

1
2
3
4
5
6
7
static void adjustlocalvars (LexState *ls, int nvars) {
FuncState *fs = ls->fs;
fs->nactvar = cast_byte(fs->nactvar + nvars);
for (; nvars; nvars--) {
getlocvar(fs, fs->nactvar - nvars)->startpc = fs->pc;
}
}

至此,第一个问题得到了解决:在函数 localstat 中,会读取=号左边的所有变量,并在 Proto 结构体中创建相应的局部变量信息;而变量在 Lua 函数栈中的存储位置存放在 freereg 变量中,它会根据当前函数栈中变量的数量进行调整。

第二个问题:表达式的结果如何存储?

解析表达式的结果会存储在一个临时数据结构 expdesc 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct expdesc {
expkind k;
union {
struct { /* for indexed variables (VINDEXED) 索引变量(VINDEXED) */
short idx; /* index (R/K) 索引 */
lu_byte t; /* table (register or upvalue) 表(寄存器或者上值) */
lu_byte vt; /* whether 't' is register (VLOCAL) or upvalue (VUPVAL) t 是寄存器(局部变量)或者上值(上值) */
} ind;
int info; /* for generic use */
lua_Number nval; /* for VKFLT */
lua_Integer ival; /* for VKINT */
} u;
int t; /* patch list of 'exit when true' */
int f; /* patch list of 'exit when false' */
} expdesc;
  • 变量 k 表示具体的类型
  • 后面紧跟的 union u 根据不同的类型存储的数据有所区分,具体可以看 expkind 类型定义后面的注释
  • 至于 t 和 f 这两个变量,目前可以暂时不管,这是跳转相关的指令。

解析表达式列表:

解析表达式列表的函数为explist:

1
2
3
4
5
6
7
8
9
10
11
static int explist (LexState *ls, expdesc *v) {
/* explist -> expr { ',' expr } */
int n = 1; /* at least one expression */
expr(ls, v);
while (testnext(ls, ',')) {
luaK_exp2nextreg(ls->fs, v);
expr(ls, v);
n++;
}
return n;
}
  • 调用函数expr解析表达式
  • 当解析的表达式列表中还存在其他的表达式时,即有逗号(,)分隔的式子时,针对每个表达式继续调用expr函数解析表达式,将结果缓存在expdesc结构体中,然后调用函数 luaK_exp2nextreg 将表达式存入当前函数的下一个可用寄存器中。

根据上面的调用路径,最终会走入 simpleexp 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void simpleexp (LexState *ls, expdesc *v) {
/* simpleexp -> FLT | INT | STRING | NIL | TRUE | FALSE | ... |
constructor | FUNCTION body | suffixedexp */
switch (ls->t.token) {
...
case TK_INT: {
init_exp(v, VKINT, 0);
v->u.ival = ls->t.seminfo.i;
break;
}
...
default: {
suffixedexp(ls, v);
return;
}
}
luaX_next(ls);
}
  • 使用类型 VKINT 初始化 expdesc 结构体,这个类型表示数字常量
  • 将具体的数据赋值给 expdesc 结构体中的 ival,前面说过,expdesc 结构体中 union u 的数据根据不同的类型会存储不同的信息,在 VKINT 这个类型下就是用来存储数字的。

现在这个表达式的信息已经存放在 expdesc 结构体中,需要进一步根据这个结构体的信息来生成对应的字节码。

这个工作由函数 luaK_exp2nextreg 完成,需要根据 expdesc 结构体生成字节码时,都要经过它:

  • 调用 luaK_dischargevars 函数,根据变量所在的不同作用域(local,global,upvalue)来决定这个变量是否需要重定向
  • 调用 luaK_reserveregs 函数,分配可用的函数寄存器空间,得到这个空间对应的寄存器索引。有了空间,才能存储变量
  • 调用exp2reg 函数,真正完成把表达式的数据放入寄存器空间的工作。在这个函数中,最终又会调用dischargereg函数,这个函数式根据不同的表达式类型(NIL,布尔表达式,数字等)来生成存取表达式的值到寄存器的字节码

在函数 discharge2reg 中最终会走到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void discharge2reg (FuncState *fs, expdesc *e, int reg) {
luaK_dischargevars(fs, e);
switch (e->k) {
...
case VKINT: {
luaK_codek(fs, reg, luaK_intK(fs, e->u.ival));
break;
}
default: {
lua_assert(e->k == VVOID || e->k == VJMP);
return; /* nothing to do... */
}
}
e->u.info = reg;
e->k = VNONRELOC;
}

在这个函数的参数中,reg 参数就是前面得到的寄存器索引,于是最后根据 k 值生成了OP_LOADK指令(另一种情况是生成OP_LOADKX指令),将数字 10 加载到 reg 参数对应的寄存器中。

至此,通过对这个最简单的向局部变量赋值操作的分析,完成了 Lua 解释器从词法分析到生成字节码的全过程分析:

  • 每个局部变量都有一个对应的 LocVar 结构体存储它的变量名信息。
  • 每个局部变量都会对应分配一个函数栈的位置来保存它的数据。
  • 解析表达式的结果会存在 expdesc 结构体中。根据不同的类型,内部使用的联合体存放的数据有不同的意义。
  • luaK_exp2nextreg 是一个非常重要的函数,它用于将 expdesc 结构体的信息中存储的表达式信息转换成对应的 opcode。

流程图:

mh9OQU.png
mh9OQU.png

如果代码变为:

1
local a,b = 10

则在localstat函数中进入:

1
2
3
4
5
static void localstat (LexState *ls) {
...
adjust_assign(ls, nvars, nexps, &e);
adjustlocalvars(ls, nvars);
}

第一个函数adjust_assign用于根据等号两边变量和表达式的数量来调整赋值。具体来说,在上面这个例子中,当变量数量多于等号右边的表达式数量时,会将多余的变量置为NIL。

第二个函数adjustlocalvars会根据变量的数量调整FuncState结构体中记录局部变量数量的nactvar对象,并记录这些局部变量的startpc值。

如果代码变为:

1
2
local a = 10
local b = a

主要区别在于走到simpleexp函数时,进入的是另一条路径,走入了primaryexp函数中。然后在prefixexp函数中,判断这是一个变量时,会调用singlevar函数(实际上,最后会调用递归函数 singlevaraux)来进行变量的查找,这个函数的大体流程如下:

  1. 如果变量在当前函数的 LocVar 结构体数组中找到,那么这个变量就是局部变量,类型为 VLOCAL。
  2. 如果在当前函数中找不到,就逐层往上面的 block 来查找,如果在某一层查到了,那么这个变量就是 UpValue,类型为 VUPVAL。
  3. 如果最后那层都没有查到,那么这个变量就是全局变量,类型为 VGLOBAL。

在 luaK_dischargevars 函数中,根据三种不同的类型有不同的操作。

如果赋值的源数据是局部变量,则使用 MOVE 指令来完成赋值。

全局变量:

1
2
a = 10
local b = a

这时对应的 luaK_dischargevars 函数这样:

1
2
3
4
5
6
7
8
9
10
11
12
void luaK_dischargevars (FuncState *fs, expdesc *e) {
switch (e->k) {
...
case VGLOBAL: {
e->u.s.info = luaK_codeABx(fs, OP_GETGLOBAL, 0, e->u.s.info);
e->k = VRELOCABLE;
break;
}
...
}
}
// lua 5.1.5

而在 Lua 5.3版本中,应该是在这部分函数中对全局变量进行处理的(存疑)。

1
2
3
4
5
6
7
8
9
10
11
static void singlevar (LexState *ls, expdesc *var) {
TString *varname = str_checkname(ls);
FuncState *fs = ls->fs;
if (singlevaraux(fs, varname, var, 1) == VVOID) { /* global name? */
expdesc key;
singlevaraux(fs, ls->envn, var, 1); /* get environment variable */
lua_assert(var->k == VLOCAL || var->k == VUPVAL);
codestring(ls, &key, varname); /* key is variable name */
luaK_indexed(fs, var, &key); /* env[varname] */
}
}

参考:

  1. 扩展巴科斯范式