有了前面的基础,现在我们来简单分析一下最简单的打印"Hellow World"语句,究竟需要哪些步骤,以此做为后面分析Lua解释器指令的热身.
简单来说,Lua虚拟机在解释执行一个Lua脚本文件,需要经过以下几个步骤:
我们接下来就以一个最简单的Lua代码:
print("Hello World")
来解释前面的步骤是如何进行的.
我在自己研究分析Lua解释器原理的时候,由于需要分不同的Lua指令来进行分析,所以我写了一个简单的C代码,根据命令行参数来读取不同的Lua文件来执行,然后我可以gdb来调试这个程序,于是就能较为方便的研究Lua解释器的工作原理.该代码如下:
#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
int main(int argc, char *argv[]) {
char *file = NULL;
if (argc == 1) {
file = "my.lua";
} else {
file = argv[1];
}
lua_State *L = lua_open();
luaL_openlibs(L);
luaL_dofile(L, file);
return 0;
}
假设上面的C代码编译生成了test,而前面的那个打印"Hello World"的Lua脚本文件名为"hello.lua",那么执行:
./test hello.lua
就可以解释执行出hello.lua中的代码,通过使用gdb等调试工具,就可以来进一步观察分析Lua解释器的行为.现在以及以后对Lua指令的分析,就是通过这个程序执行不同的Lua脚本文件来进行分析的.
在开始下面的分析之前,首先来介绍一下两个重要的数据结构:lua_State以及global_State.
lua_State可以认为是每一个Lua"线程"所独有的一份数据,后面介绍到Lua协程时会明白这里所谓的"线程"是什么意思.
将其中各个成员变量分类介绍以含义如下:
前面提过,每次函数调用都对应一个CallInfo结构体,
global_State是一个进程独有的数据结构,它其中的很多数据会被该进程中所有的lua_State所共享.换言之,一个进程只会有一个global_State,但是却可能有多份lua_State,它们之间是一对多的关系.
65 /*
66 ** `global state', shared by all threads of this state
67 */
68 typedef struct global_State {
69 stringtable strt; /* hash table for strings */
70 lua_Alloc frealloc; /* function to reallocate memory */
71 void *ud; /* auxiliary data to `frealloc' */
72 lu_byte currentwhite;
73 lu_byte gcstate; /* state of garbage collector */
74 int sweepstrgc; /* position of sweep in `strt' */
75 GCObject *rootgc; /* list of all collectable objects */
76 GCObject **sweepgc; /* position of sweep in `rootgc' */
77 GCObject *gray; /* list of gray objects */
78 GCObject *grayagain; /* list of objects to be traversed atomically */
79 GCObject *weak; /* list of weak tables (to be cleared) */
80 GCObject *tmudata; /* last element of list of userdata to be GC */
81 Mbuffer buff; /* temporary buffer for string concatentation */
82 lu_mem GCthreshold;
83 lu_mem totalbytes; /* number of bytes currently allocated */
84 lu_mem estimate; /* an estimate of number of bytes actually in use */
85 lu_mem gcdept; /* how much GC is `behind schedule' */
86 int gcpause; /* size of pause between successive GCs */
87 int gcstepmul; /* GC `granularity' */
88 lua_CFunction panic; /* to be called in unprotected errors */
89 TValue l_registry;
90 struct lua_State *mainthread;
91 UpVal uvhead; /* head of double-linked list of all open upvalues */
92 struct Table *mt[NUM_TAGS]; /* metatables for basic types */
93 TString *tmname[TM_N]; /* array with tag-method names */
94 } global_State;
首先来看第一步,调用lua_open函数创建并且初始化一个Lua虚拟机指针,来看看这里具体做了什么事情.
lua_open实际上是一个宏,其最终会调用函数luaL_newstate来创建一个Lua_State指针,这里主要完成的是lua_State结构体的初始化及其成员变量以及global_State结构体的初始化工作.
初始化完毕lua_State结构体之后,下一步就是读取Lua脚本文件,进行词法分析->语法分析->语义分析,最后生成Lua虚拟机指令.这一步的入口是调用luaL_dofile,这也是一个宏:
(lauxlib.h)
111 #define luaL_dofile(L, fn) \
112 (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
可以看到,这个宏包括了两个函数调用,首先调用luaL_loadfile函数,这个函数主要是对Lua脚本进行上述所说的分析,而只有在这个函数返回0也就是调用成功的时候,才会调用lua_pcall函数,这个函数将会根据上一步成功分析完毕之后生成的Lua虚拟机指令来执行.下面依次来看看这几个过程中涉及到的重要函数和数据结构.
这一步涵盖了词法分析,语法分析以及语义分析,Lua解释器的一个特点这几个过程是一遍遍历完成的,因此Lua在这方面的效率很高,但是这也给分析这部分代码带来一定难度. 这部分最终的入口函数是f_parser函数,这里首先不具体深入分析各个分析过程,而是先来具体看看这个函数的代码,明白这个函数执行完毕之后对外部的输出,以及相关的数据结构:
(ldo.c)
490 static void f_parser (lua_State *L, void *ud) {
491 int i;
492 Proto *tf;
493 Closure *cl;
494 struct SParser *p = cast(struct SParser *, ud);
495 int c = luaZ_lookahead(p->z);
496 luaC_checkGC(L);
497 tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z,
498 &p->buff, p->name);
499 cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
500 cl->l.p = tf;
501 for (i = 0; i < tf->nups; i++) /* initialize eventual upvalues */
502 cl->l.upvals[i] = luaF_newupval(L);
503 setclvalue(L, L->top, cl);
504 incr_top(L);
505 }
首先看到的是SParser结构体,这个结构体只会在分析过程中使用,在它内部的成员变量有已经读取进内存的Lua脚本文件内容,文件名称等数据.这里将调用luaZ_lookahead函数拿到一个int型数据,以此来判断即将进行分析的内容,是已经经过了luac预编译的二进制内容,还是Lua脚本,根据不同的情况调用不同的parser来进行分析.
第一步分析的结果,最终会返回一个Proto结构体指针,这是一个重要的数据结构,它是用来存放分析完一个Lua函数之后,包括指令,参数,Upvalue等信息,来看看其成员变量的具体含义:
CommonHeader:Lua通用数据相关的成员,前面已经做过讲解.
TValue *k: 存放常量的数组.
Instruction *code:存放指令的数组.
struct Proto **p:如果本函数中还有内部定义的函数,那么这些内部函数相关的Proto指针就放在这个数组里.
int *lineinfo:存放对应指令的行号信息,与前面的code数组内的元素一一对应.
struct LocVar *locvars:存放函数内局部变量的数组.
TString **upvalues:存放UpValue的数组,至于UpValue的含义,后面会做分析.
TString *source:Lua脚本文件名.
int sizeupvalues:前面upvalues数组的大小.
int sizek:k数组的大小.
int sizecode:code数组的大小.
int sizelineinfo:lineinfo数组的大小.
int sizep:p数组的大小.
int sizelocvars:localvars数组的大小.
int linedefined:函数body的第一行行号.
int lastlinedefined:函数body的最后一行.
GCObject *gclist:gc链表.
lu_byte nups:upvalue的数量.
lu_byte numparams:函数参数数量.
lu_byte is_vararg:有以下几种取值
#define VARARG_HASARG 1
#define VARARG_ISVARARG 2
#define VARARG_NEEDSAR 4
由上面的分析可知,当执行Lua解释器对一个Lua脚本文件进行分析之后,最终返回的就是一个Proto结构体指针,但是需要注意的是,前面对Proto结构体的说明中提过,这个结构体与Lua函数一一对应,但是这个说法并不是特别精确,比如我们这里的测试代码打印一个"Hello World"字符串,这就不是一个函数,但是即便如此,分析完这个文件之后也会有一个对应的Proto结构体,因此需要把Lua函数的概念扩大到Lua模块,可以说一个Lua模块也是一个Lua函数,对一个Lua模块分析也是相当于对一个Lua函数进行分析,只不过这个函数没有参数,没有函数名.
继续下面的分析,返回Proto指针之后,将会创建一个Closure结构体:
(lobject.h)
309 typedef union Closure {
310 CClosure c;
311 LClosure l;
312 } Closure;
从它的定义可知,它既可以用来存放C函数相关信息,也可以用来存放Lua函数相关信息.在这里当然是保存的Lua函数信息,主要就是要将返回的Proto信息保存下来,它将在后面Lua虚拟机执行指令时用到.
最后需要注意的是f_parser函数的最后两句代码,它将Closure指针push到了Lua栈中,同时将栈指针加1,由此可知分析完Lua脚本文件产生的Closure指针一定是在Lua栈顶.
到了这里,已经将分析Lua脚本文件的流程和其中重要的数据结构做了概述,也就是luaL_loadfile函数调用的过程,接下来就是分析如何根据这一步的结果来执行Lua虚拟机指令了.
调用luaL_loadfile并且成功返回之后,产生的Closure指针被放在了Lua栈顶,这时需要调用lua_pcall(L, 0, LUA_MULTRET, 0)执行Lua指令.这里对lua_pcall函数参数做一些解释,第一个参数自不必解释;第二个参数表示调用这个函数时的参数数量,前面已经提到过,对一个Lua文件进行分析时也可以看做是分析一个Lua函数,只不过其函数参数数量为0,因此此处传递进来的第二个参数是0;第三个参数表示的该函数返回参数的数量,调用完毕之后将根据这个参数对Lua栈进行调整,同样的由于这里是一个Lua文件,所以需要传递的是LUA_MULTRET这个常量,表示多返回值;最后一个参数表示的是出错处理函数在Lua栈中得位置,只有非0值才有意义.
lua_pcall函数最终会进入luaD_call函数来,可是在此之前,首先需要拿到所调用函数相关的Closure指针,这是如何拿到的呢?前面提到过,f_parser返回的Closure指针会放在Lua栈顶,在lua_pcall中有这么一行代码:
(lapi.c)
819 c.func = L->top - (nargs+1); /* function to be called */
上面分析lua_pcall函数参数的时候提到过,这里传入的nargs参数是0,因为这里是分析一个Lua文件所以没有函数参数,所以这个操作就拿到了上一步中Closure指针在Lua栈中的地址.同时,从这个操作可以知道,在调用一个Lua函数时,该函数的Lua栈从下往上依次是函数Closure,函数的各个参数,这个问题后面具体分析到Lua函数调用再进行展开分析.
继续看看luaD_call函数:
(ldo.c)
369 void luaD_call (lua_State *L, StkId func, int nResults) {
370 if (++L->nCcalls >= LUAI_MAXCCALLS) {
371 if (L->nCcalls == LUAI_MAXCCALLS)
372 luaG_runerror(L, "C stack overflow");
373 else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3)))
374 luaD_throw(L, LUA_ERRERR); /* error while handing stack error */
375 }
376 if (luaD_precall(L, func, nResults) == PCRLUA) /* is a Lua function? */
377 luaV_execute(L, 1); /* call it */
378 L->nCcalls--;
379 luaC_checkGC(L);
380 }
这里首先对Lua函数调用层次做判断,接下来调用luaD_precall进行函数调用之前的一些准备工作,这里不展开讨论,简单的说其做的工作有这些:准备好Lua函数栈,创建CallInfo指针并且根据相应信息进行初始化.
到这里,准备工作已经做完了,下面就是调用luaV_execute逐个取出Lua指令来执行:
(lvm.c)
373 void luaV_execute (lua_State *L, int nexeccalls) {
374 LClosure *cl;
375 StkId base;
376 TValue *k;
377 const Instruction *pc;
378 reentry: /* entry point */
379 lua_assert(isLua(L->ci));
380 pc = L->savedpc;
381 cl = &clvalue(L->ci->func)->l;
382 base = L->base;
383 k = cl->p->k;
384 /* main loop of interpreter */
385 for (;;) {
386 const Instruction i = *pc++;
/* 执行Lua指令 */
761 }
762 }
可以看到,luaV_execute函数首先根据CallInfo指针拿到LClosure指针,然后pc指针指向savedpc指针,再逐个从这个pc指针中拿出指令来逐条执行.而这个savedpc指针式在哪里进行初始化的呢,在luaD_precall函数中:
(ldo.c)
293 L->savedpc = p->code; /* starting point */
上面的p就是分析Lua脚本之后生成的Proto指针,而它的code成员变量前面提到过存放的就是分析后的指令.
以上可以看出,其实Proto结构体才是Lua解释器分析Lua脚本过程中最重要的数据结构,而Closure,CallInfo结构体不过是这个过程为了提供给Lua虚拟机执行指令装载Proto结构体用的中间载体,最终用到的还是Proto结构体中得数据.
以上,将如何加载,分析,以及到最终执行Lua指令中间涉及到的重要函数,数据结构做了一些分析,碍于目前的进度,还没有对一些过程进行深入讨论,但是已经对Lua虚拟机的整体流程有了概观的了解,有了骨架,后面就是根据具体的知识点来进行深入分析了.