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 static void f_parser (lua_State *L, void *ud) { LClosure *cl; struct SParser *p = cast(struct SParser *, ud); int c = zgetc(p->z); 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 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 ); if (k == NULL || L->nny > 0 ) { c.nresults = nresults; status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); } else { CallInfo *ci = L->ci; ci->u.c.k = k; ci->u.c.ctx = ctx; ci->extra = savestack(L, c.func); ci->u.c.old_errfunc = L->errfunc; L->errfunc = func; setoah(ci->callstatus, L->allowhook); ci->callstatus |= CIST_YPCALL; luaD_call(L, c.func, nresults, 1 ); ci->callstatus &= ~CIST_YPCALL; L->errfunc = ci->u.c.old_errfunc; status = LUA_OK; } adjustresults(L, nresults); lua_unlock(L); return status; }
可以看到,首先获取需要调用的函数指针:
1 c.func = L->top - (nargs+1 );
这里的 nargs 是由函数参数传入的,在 lual_dofile 中调用 lua_pcall 时,这里传入的参数是 0。换句话说,这里得到的函数对象指针就是前面 f_parser 函数中最后两句代码放入 Lua 栈的 Closure 指针。
继续向下执行,在调用函数 luaD_pcall 时,最终会执行到 luaD_call 函数,其中有:
1 2 if (!luaD_precall(L, func, nResults)) luaV_execute(L);
首先调用 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 int luaD_precall (lua_State *L, StkId func, int nresults) { lua_CFunction f; CallInfo *ci; int n; ptrdiff_t funcr = savestack(L, func); switch (ttype(func)) { ... case LUA_TLCL: { StkId base; Proto *p = clLvalue(func)->p; ... ci = next_ci(L); 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; ci->callstatus = CIST_LUA; L->top = ci->top; luaC_checkGC(L); 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 void luaV_execute (lua_State *L) { CallInfo *ci = L->ci; LClosure *cl; TValue *k; StkId base; newframe: lua_assert(ci == L->ci); cl = clLvalue(ci->func); k = cl->p->k; base = ci->u.l.base; 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)); } 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); luaD_hook(L, LUA_HOOKRET, -1 ); firstResult = restorestack(L, fr); } L->oldpc = ci->previous->u.l.savedpc; } res = ci->func; wanted = ci->nresults; L->ci = ci = ci->previous; 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); }
虚拟机执行流程:
在f_parser函数中,对代码文件的分析返回了Proto指针。这个指针会保存在Closure指针中,留待后续继续使用。
在luaD_precall函数中,将lua_state的savedpc指针指向第一步中Proto结构体的code指针,同时准备好函数调用时的栈信息。
在luaV_execute函数中,pc指针指向第二步的savedpc指针(Lua5.3中似乎直接使用的savedpc指针),紧跟着就是一个大的循环体,依次取出其中的OpCode执行。
执行完毕后,调用luaD_poscall函数恢复到上一个函数的环境。
虚拟机指令执行的两大入口如下:
词法、语法分析阶段的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 typedef struct Proto { CommonHeader; lu_byte numparams; lu_byte is_vararg; lu_byte maxstacksize; int sizeupvalues; int sizek; int sizecode; int sizelineinfo; int sizep; int sizelocvars; int linedefined; int lastlinedefined; TValue *k; Instruction *code; struct Proto **p ; int *lineinfo; LocVar *locvars; Upvaldesc *upvalues; struct LClosure *cache ; TString *source; 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