Lua源码阅读:虚拟机概述

Lua 虚拟机的执行过程。

Lua 执行过程概述:

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

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

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

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

虚拟机的实现方式:

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

区别:

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

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

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

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

1  
2  
	(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  (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

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

  • 词法、语法分析阶段的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

糖果

糖果
LUA教程

Lapis框架的常用处理方法

Lapis框架的常用处理方法 Continue reading

MoonScript实现选择排序

Published on February 26, 2017

MoonScript与Redis客户端

Published on January 19, 2017