Lua 虚拟机的执行过程。

Lua 执行过程概述:

解释型脚本语言与编译型语言的区别如下:

  • 由于每个脚本语言都有自己的一套字节码,与具体的硬件平台无关,所以不用修改脚本代码,就能运行在各个平台上。硬件、软件平台的差异都由语言自身的虚拟机解决。
  • 由于脚本语言的字节码需要虚拟机执行,而不像机器代码能够直接执行,所以运行速度比编译型语言差不少。

虚拟机需要完成以下工作:

  • 将源代码编译成虚拟机可以识别执行的字节码。
  • 为函数调用准备调用栈。
  • 内部维持一个 IP(Instruction Pointer,指令指针)来保存下一个将执行的指令地址。在 Lua 代码中,IP 对应的是 PC 指针。
  • 模拟一个 CPU 的运行:循环拿出由 IP 指向的字节码,根据字节码格式进行解码,然后执行字节码。

虚拟机的实现方式:

虚拟机有两种不同的实现方式:基于栈的虚拟机和基于寄存器的虚拟机。Lua 是基于寄存器虚拟机的语言。

区别:

  • 在基于栈的虚拟机中,字节码的操作数是从栈顶上弹出(pop),在执行完操作后再压入栈顶的,这样的缺点是会多出几条指令来准备数据,优点是指令中不需要关心操作数的地址,在执行操作之前就已经将操作数准备在栈顶上了。
  • 在基于寄存器的指令中,操作数是放在“CPU 的寄存器”中,因此,同样的操作不再需要 PUSH、POP 指令,取而代之的是在字节码中带上具体操作数所在的寄存器地址。对比需要栈的寄存器,这里的指令数会减少,但缺点是此时程序需要关注操作数所在的位置。

实现一个脚本语言的解释器:

  • 设计一套字节码,分析源代码文件生成字节码。
  • 在虚拟机中执行字节码。
  • 如何在整个执行过程中保存整个执行环境。

执行Lua文件调用的是luaL_dofile 函数:

1
2
#define luaL_dofile(L, fn) \
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

其中 luaL_loadfile 函数用于进行词法和语法分析,lua_pcall 用于将第一步中分析的结果(字节码)放在虚拟机中执行。

luaL_loadfile 函数,通过lua_load,luaD_protectedparser,最终调用 f_parser 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ldo.c

static void f_parser (lua_State *L, void *ud) {
LClosure *cl;
struct SParser *p = cast(struct SParser *, ud);
int c = zgetc(p->z); /* read first character */
if (c == LUA_SIGNATURE[0]) {
checkmode(L, p->mode, "binary");
cl = luaU_undump(L, p->z, &p->buff, p->name);
}
else {
checkmode(L, p->mode, "text");
cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
}
lua_assert(cl->nupvalues == cl->p->sizeupvalues);
luaF_initupvals(L, cl);
}

完成词法分析之后,返回了 Proto 类型的指针tf,然后将其绑定在新创建的 Closure 指针上,初始化 Upvalue,最后压入栈中。

词法分析之后产生的字节码等相关数据都在这个 Proto 类型的结构体中,而这个数据又作为 Closure 保存了下来。

lua_pcall 内部调用了lua_pcallk函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// lapi.c

LUA_API int lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc,
lua_KContext ctx, lua_KFunction k) {
struct CallS c;
int status;
ptrdiff_t func;
lua_lock(L);
api_check(k == NULL || !isLua(L->ci),
"cannot use continuations inside hooks");
api_checknelems(L, nargs+1);
api_check(L->status == LUA_OK, "cannot do calls on non-normal thread");
checkresults(L, nargs, nresults);
if (errfunc == 0)
func = 0;
else {
StkId o = index2addr(L, errfunc);
api_checkstackindex(errfunc, o);
func = savestack(L, o);
}
c.func = L->top - (nargs+1); /* function to be called */
if (k == NULL || L->nny > 0) { /* no continuation or no yieldable? */
c.nresults = nresults; /* do a 'conventional' protected call */
status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
}
else { /* prepare continuation (call is already protected by 'resume') */
CallInfo *ci = L->ci;
ci->u.c.k = k; /* save continuation */
ci->u.c.ctx = ctx; /* save context */
/* save information for error recovery */
ci->extra = savestack(L, c.func);
ci->u.c.old_errfunc = L->errfunc;
L->errfunc = func;
setoah(ci->callstatus, L->allowhook); /* save value of 'allowhook' */
ci->callstatus |= CIST_YPCALL; /* function can do error recovery */
luaD_call(L, c.func, nresults, 1); /* do the call */
ci->callstatus &= ~CIST_YPCALL;
L->errfunc = ci->u.c.old_errfunc;
status = LUA_OK; /* if it is here, there were no errors */
}
adjustresults(L, nresults);
lua_unlock(L);
return status;
}

可以看到,首先获取需要调用的函数指针:

1
c.func = L->top - (nargs+1);  /* function to be called */

这里的 nargs 是由函数参数传入的,在 lual_dofile 中调用 lua_pcall 时,这里传入的参数是 0。换句话说,这里得到的函数对象指针就是前面 f_parser 函数中最后两句代码放入 Lua 栈的 Closure 指针。

继续向下执行,在调用函数 luaD_pcall 时,最终会执行到 luaD_call 函数,其中有:

1
2
if (!luaD_precall(L, func, nResults))  /* is a Lua function? */
luaV_execute(L); /* call it */

首先调用 luaD_precall 函数进行执行前的准备工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
** returns true if function has been executed (C function)
...部分有省略
*/
int luaD_precall (lua_State *L, StkId func, int nresults) {
lua_CFunction f;
CallInfo *ci;
int n; /* number of arguments (Lua) or returns (C) */
ptrdiff_t funcr = savestack(L, func);
switch (ttype(func)) {
...
case LUA_TLCL: { /* Lua function: prepare its call */
StkId base;
Proto *p = clLvalue(func)->p;
...
ci = next_ci(L); /* now 'enter' new function */
ci->nresults = nresults;
ci->func = func;
ci->u.l.base = base;
ci->top = base + p->maxstacksize;

lua_assert(ci->top <= L->stack_last);

ci->u.l.savedpc = p->code; /* starting point */
ci->callstatus = CIST_LUA;

L->top = ci->top;
luaC_checkGC(L); /* stack grow uses memory */
if (L->hookmask & LUA_MASKCALL)
callhook(L, ci);
return 0;
}
...
}
}
  • 从 lua_State 的 CallInfo 数组中得到一个新的 CallInfo 结构体,设置它的func、base、top 指针
  • 从前面分析阶段生成的 Closure 指针中,取出保存下来的 Proto 结构体。这个结构体中保存的是分析过程完结之后生成的字节码等信息。
  • 将这里创建的 CallInfo 指针的 top、base 指针赋给 lua_State 结构体的 top、base 指针。将 Proto 结构体的 code 成员赋值给 lua_State 指针的 savedpc 字段,code 成员保留的就是字节码。
  • 将多余的函数参数赋值为 nil,比如一个函数定义中需要的是两个参数,实际传入的只有一个,那么多出来的那个参数会被赋值为 nil。

执行完 luaD_precall 函数之后,接着会进入 luaV_execute 函数,这里是虚拟机执行代码的主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// lvm.c
void luaV_execute (lua_State *L) {
CallInfo *ci = L->ci;
LClosure *cl;
TValue *k;
StkId base;
newframe: /* reentry point when frame changes (call/return) */
lua_assert(ci == L->ci);
cl = clLvalue(ci->func);
k = cl->p->k;
base = ci->u.l.base;
/* main loop of interpreter */
for (;;) {
Instruction i = *(ci->u.l.savedpc++);
StkId ra;
if ((L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) &&
(--L->hookcount == 0 || L->hookmask & LUA_MASKLINE)) {
Protect(luaG_traceexec(L));
}
/* WARNING: several calls may realloc the stack and invalidate 'ra' */
ra = RA(i);
/* 后面是各种字节码的处理流程 */
}

这里的 ci->u.l.savedpc存放的是虚拟机 OpCode 代码,这部分从 L->savepc 初始化而来,而 L->savepc 在 luaD_precall 中赋值。可以看到,luaV_execute 函数最主要的作用就是一个大循环,将当前传入的指令依次执行。

执行完毕后,会调用 luaD_poscall 函数恢复到上一次函数调用的环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int luaD_poscall (lua_State *L, StkId firstResult) {
StkId res;
int wanted, i;
CallInfo *ci = L->ci;
if (L->hookmask & (LUA_MASKRET | LUA_MASKLINE)) {
if (L->hookmask & LUA_MASKRET) {
ptrdiff_t fr = savestack(L, firstResult); /* hook may change stack */
luaD_hook(L, LUA_HOOKRET, -1);
firstResult = restorestack(L, fr);
}
L->oldpc = ci->previous->u.l.savedpc; /* 'oldpc' for caller function */
}
res = ci->func; /* res == final position of 1st result */
wanted = ci->nresults;
L->ci = ci = ci->previous; /* back to caller */
/* move results to correct place */
for (i = wanted; i != 0 && firstResult < L->top; i--)
setobjs2s(L, res++, firstResult++);
while (i-- > 0)
setnilvalue(res++);
L->top = res;
return (wanted - LUA_MULTRET); /* 0 iff wanted == LUA_MULTRET */
}

虚拟机执行流程:

  1. 在f_parser函数中,对代码文件的分析返回了Proto指针。这个指针会保存在Closure指针中,留待后续继续使用。
  2. 在luaD_precall函数中,将lua_state的savedpc指针指向第一步中Proto结构体的code指针,同时准备好函数调用时的栈信息。
  3. 在luaV_execute函数中,pc指针指向第二步的savedpc指针(Lua5.3中似乎直接使用的savedpc指针),紧跟着就是一个大的循环体,依次取出其中的OpCode执行。
  4. 执行完毕后,调用luaD_poscall函数恢复到上一个函数的环境。
FlowchartDiagram2.png
FlowchartDiagram2.png

虚拟机指令执行的两大入口如下:

  • 词法、语法分析阶段的luaY_parser,Lua一次遍历脚本文件完成了词法分析和语法分析,生成的OpCode存放在Proto结构体的code数组中。
  • luaV_execute:虚拟机执行指令阶段的入口函数,取出第一步生成的Proto结构体中的指令执行。

Proto结构体中的数据:

  • 函数的常量数组
  • 编译生成的字节码信息
  • 函数的局部变量信息
  • 保存upvalue名字的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
** Function Prototypes
*/
typedef struct Proto {
CommonHeader;
lu_byte numparams; /* number of fixed parameters */
lu_byte is_vararg;
lu_byte maxstacksize; /* maximum stack used by this function */
int sizeupvalues; /* size of 'upvalues' */
int sizek; /* size of 'k' */
int sizecode;
int sizelineinfo;
int sizep; /* size of 'p' */
int sizelocvars;
int linedefined;
int lastlinedefined;
TValue *k; /* constants used by the function */
Instruction *code;
struct Proto **p; /* functions defined inside the function */
int *lineinfo; /* map from opcodes to source lines (debug information) */
LocVar *locvars; /* information about local variables (debug information) */
Upvaldesc *upvalues; /* upvalue information */
struct LClosure *cache; /* last created closure with this prototype */
TString *source; /* used for debug information */
GCObject *gclist;
} Proto;

该程序最终会调用 luaV_execute() 函数执行,开始会初始化 global_State、lua_State 两个结构体,用来保存上下文的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
main()
|-luaL_newstate() # 创建global_State+lua_State,并初始化
|-lua_pcall() # 实际会调用pmain()函数
| # 根据不同的参数调用不同的函数
|-runargs() # 执行命令行通过-e指定的命令
|-doREPL() # 执行交互模式,也即read-eval-print loop
|-handle_script() # 执行lua脚本
|-luaL_loadfile() # 加载lua文件,后面详细介绍
| |-lua_load()
|
|-docall() # 调用执行
|-lua_pcall()
|-luaD_pcall() # 实际会调用f_call()函数

在调用函数执行过程中,最终会调用 luaV_execute() 函数。
其中,主要处理字节码的是 for(;;){} 循环,也即进入到解释器的主循环,处理很简单,取得当前指令,pc 递增,初始化 ra,然后根据指令的操作码进行选择;然后接下来是一大串的 switch … case … 处理。

接下来对其中有主要的几类指令进行说明。


关于define部分符号用法详见:https://www.jianshu.com/p/e9f00097904a,先转一个,有时间了再写。

另一个参考:https://gcc.gnu.org/onlinedocs/cpp/Macros.html#Macros