Lua 虚拟机执行环境的初始化流程

作为下一篇 opcode 的「餐前甜品」,我们先分析下 Lua 虚拟机的初始化流程,为执行 opcode 做好准备。本次的文章从创建虚拟机开始,直至 luaV_execute 执行 opcode,将覆盖除了词法、语法解析之外的大多数源码。

说起来,写这篇文章的时候刚好是我新冠第一天,浑身酸痛怕冷无力,但还是间断性地坚持写完了。感谢找上门来的“优质毒株”,我的症状在同事朋友里其实已经算相对很轻了~

从 etc/min.c 开始

etc/min.c 下提供了一个最小的 Lua 解释器,它没有内置函数,也没有 Lua 的各种内置库,唯一提供的只有一个简陋的 print 函数,以及 Lua 语言核心的语法。在此目录下执行 make min 即可完成编译。这个解释器直接将 stdin 作为 Lua 脚本执行,类似这样:

$ echo "print('chenxy.me')" | ./a.out
chenxy.me

整个文件非常简单,只有几十行,如下:

/*
* min.c -- a minimal Lua interpreter
* loads stdin only with minimal error handling.
* no interaction, and no standard library, only a "print" function.
*/

#include <stdio.h>

#include "lua.h"
#include "lauxlib.h"

static int print(lua_State *L)
{
 // ... 省略
}

int main(void)
{
 lua_State *L=lua_open();
 lua_register(L,"print",print);
 if (luaL_dofile(L,NULL)!=0) fprintf(stderr,"%s\n",lua_tostring(L,-1));
 lua_close(L);
 return 0;
}

核心就是依次对四个函数的调用,lua_openlua_registerluaL_dofilelua_close。我们挑出 lua_openluaL_dofile 来做下分析。

lua_open 创建全局与线程状态表

lua_open 函数是展开为 luaL_newstate 的一个宏,后者的代码:

LUALIB_API lua_State *luaL_newstate (void) {
  lua_State *L = lua_newstate(l_alloc, NULL);
  if (L) lua_atpanic(L, &panic);
  return L;
}

其中 lua_atpanic 是错误处理相关的,暂且不管,继续往下追 lua_newstate

// 创建并初始化一个新的 lua_State 对象(以及 global_State 对象)
// 用于创建主线程时一起创建 lua_State 与 global_State
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
  int i;
  lua_State *L;
  global_State *g;
  // 用传入的 lua_Alloc 函数创建一块内存用以放置 lua_State 跟 global_State
  // state_size 展开来是 (sizeof(LG) + LUAI_EXTRASPACE)
  // LG 是一个仅包含了 lua_State 跟 global_State 的结构体
  // LUAI_EXTRASPACE 是一个用户可以指定的常量,默认为 0,作用是定制 Lua 解释器时可以借助这个宏在 lua_State 里扩充自己的字段
  void *l = (*f)(ud, NULL, 0, state_size(LG));
  // 这里创建出来是 lua_State + global_State + LUAI_EXTRASPACE(默认为0) 的大小
  // 当另开新线程时,使用的是 lua_newthread,仅创建 lua_State,与已有的线程共享 global_State
  if (l == NULL) return NULL;
  L = tostate(l);
  g = &((LG *)L)->g;
  // 清空、初始化 L 跟 g 的各种字段
  // GC 部分,标记为白色,等后边研究 GC 再讨论
  L->next = NULL;
  L->tt = LUA_TTHREAD;
  g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT);
  L->marked = luaC_white(g);
  // 标记刚创建的 lua_State 对象为不可回收
  set2bits(L->marked, FIXEDBIT, SFIXEDBIT);
  // 主要初始化一些栈相关的字段
  preinit_state(L, g);
  g->frealloc = f;
  g->ud = ud;
  // 全局状态结构体里,主线程置为当前 lua_State
  g->mainthread = L;
  g->uvhead.u.l.prev = &g->uvhead;
  g->uvhead.u.l.next = &g->uvhead;
  g->GCthreshold = 0;  /* mark it as unfinished state */
  // 全局字符串表,Lua 里的字符串都是「内化」(interned)的
  // 所有相同取值的字符串有且仅有一个实例,被记录在 strt 全局字符串表中
  g->strt.size = 0;
  g->strt.nuse = 0;
  g->strt.hash = NULL;
  setnilvalue(registry(L));
  luaZ_initbuffer(L, &g->buff);
  g->panic = NULL;
  // GC 相关的字段
  g->gcstate = GCSpause;
  g->rootgc = obj2gco(L);
  g->sweepstrgc = 0;
  g->sweepgc = &g->rootgc;
  g->gray = NULL;
  g->grayagain = NULL;
  g->weak = NULL;
  g->tmudata = NULL;
  g->totalbytes = sizeof(LG);
  g->gcpause = LUAI_GCPAUSE;
  g->gcstepmul = LUAI_GCMUL;
  g->gcdept = 0;
  // 清空全局元表
  // g->mt 的元素与各个类型一一对应,如 LUA_TNUMBER、LUA_TSTRING 等
  for (i=0; i<NUM_TAGS; i++) g->mt[i] = NULL;
  if (luaD_rawrunprotected(L, f_luaopen, NULL) != 0) {
    /* memory allocation error: free partial state */
    close_state(L);
    L = NULL;
  }
  else
    luai_userstateopen(L);
  return L;
}

其中的 preinit_state 如下:

static void preinit_state (lua_State *L, global_State *g) {
  // 将 L 里的 l_G 指针置为 g
  G(L) = g;
  // 主要清空各种与栈相关的字段
  L->stack = NULL;
  L->stacksize = 0;
  L->errorJmp = NULL;
  L->hook = NULL;
  L->hookmask = 0;
  L->basehookcount = 0;
  L->allowhook = 1;
  resethookcount(L);
  L->openupval = NULL;
  L->size_ci = 0;
  L->nCcalls = L->baseCcalls = 0;
  L->status = 0;
  L->base_ci = L->ci = NULL;
  L->savedpc = NULL;
  L->errfunc = 0;
  // 清空 global table
  setnilvalue(gt(L));
}

完成各个字段的清空之后,会进入 f_luaopen 函数,这个函数会完成栈、全局表、寄存器、字符串表、tag method 名称列表、关键字字符串等,如下:

/*
** open parts that may cause memory-allocation errors
*/
// 仅存在一处调用,发生在 lua_newstate 函数中
static void f_luaopen (lua_State *L, void *ud) {
  global_State *g = G(L);
  UNUSED(ud);
  // 初始化堆栈
  stack_init(L, L);  /* init stack */
  // 初始化全局表
  sethvalue(L, gt(L), luaH_new(L, 0, 2));  /* table of globals */
  // 初始化寄存器 TODO
  sethvalue(L, registry(L), luaH_new(L, 0, 2));  /* registry */
  // 初始化字符串表
  luaS_resize(L, MINSTRTABSIZE);  /* initial size of string table */
  // 初始化 tag method 名称列表
  luaT_init(L);
  // 初始化关键字字符串,标记为 reserved
  luaX_init(L);
  // 初始化not enough memory这个字符串并且标记为不可回收
  luaS_fix(luaS_newliteral(L, MEMERRMSG));
  g->GCthreshold = 4*g->totalbytes;
}

其他函数都比较简单,我们主要关注下栈的初始化过程:

// 此函数目前存在两处调用点,一处是创建主线程的 f_luaopen 函数中,一处是创建新线程的 luaE_newthread 函数中
// L1 是新的 lua_State,L 是旧的 lua_State
// 当创建主线程时,两个参数指向的都是同一个 lua_State
static void stack_init (lua_State *L1, lua_State *L) {
  /* initialize CallInfo array */
  // 初始化 CallInfo 数组
  L1->base_ci = luaM_newvector(L, BASIC_CI_SIZE, CallInfo);
  L1->ci = L1->base_ci;
  L1->size_ci = BASIC_CI_SIZE;
  L1->end_ci = L1->base_ci + L1->size_ci - 1;
  /* initialize stack array */
  // 初始化堆栈数组
  L1->stack = luaM_newvector(L, BASIC_STACK_SIZE + EXTRA_STACK, TValue);
  L1->stacksize = BASIC_STACK_SIZE + EXTRA_STACK;
  L1->top = L1->stack;
  L1->stack_last = L1->stack+(L1->stacksize - EXTRA_STACK)-1;
  /* initialize first ci */
  // 初始化首个 CallInfo
  // 将 func 指向 L1->top,也就是 L1->stack,也就是栈底首个元素
  L1->ci->func = L1->top;
  // 将刚才 func 指向的区域给置为 nil,并自增 top
  setnilvalue(L1->top++);  /* `function' entry for this `ci' */
  // 执行这句调用之后, base = top = stack + 1
  // 此时栈上的结构
  //              ???   -+
  //              ...    |  LUA_MINSTACK (20)
  // top, base -> ???   -+
  // ci->func  -> nil
  L1->base = L1->ci->base = L1->top;
  // 这里的意思是,每个lua函数最开始预留LUA_MINSTACK个栈位置,不够的时候再增加,见luaD_checkstack函数
  L1->ci->top = L1->top + LUA_MINSTACK;
}

至此,我们就完成了全局状态表 global_State 以及当前线程状态表 lua_State 各个字段初始化(目前大多还只是清零而已)。在后边的 luaL_dofile 中还会进一步创建栈、全局字符串表等,继续分析:

luaL_dofile 载入文件并执行

// 先载入+解析,然后调用
#define luaL_dofile(L, fn) \
	(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

背后是一个宏,分别调用了 luaL_loadfilelua_pcall。其中,luaL_loadfile 的调用链依次是:

  • luaL_loadfile 处理 shebang,区分字节码与 Lua 文本
  • lua_load
  • luaD_protectedparser
  • luaD_pcall(f_parser)
  • f_parser
  • luaY_parser 完成 Lua 文本代码的解析

我们今天不涉及词法、语法分析的流程,这里就一笔带过,其最终产出就是把解析到的字节码等信息放置于栈上,供下一个函数 lua_pcall 进行真正的执行流程。我们继续看看后者的源码:

// lua_pcall(L, 0, LUA_MULTRET /* -1 */, 0)
LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) {
  struct CallS c;
  int status;
  ptrdiff_t func;
  lua_lock(L);
  // 检查栈顶到栈底的空间大小,至少要能容纳 nargs(参数数量) + 1(函数指针)
  api_checknelems(L, nargs+1);
  // Expands to: api_check(L, (nresults) == LUA_MULTRET || (L->ci->top - L->top >= (nresults) - (nargs)))
  // 要么传入特殊值 LUA_MULTRET,要么当前 ci 的栈预留好足够放置 nresults 的空间
  checkresults(L, nargs, nresults);
  if (errfunc == 0)
    func = 0;
  else {
    StkId o = index2adr(L, errfunc);
    api_checkvalidindex(L, o);
    func = savestack(L, o);
  }
  // 目前栈上空间长这样:
  // top   -> ???     [high]
  //          arg       |
  //          func      |
  // base  -> ???     [low]
  // c.func 指向将要执行的函数
  c.func = L->top - (nargs+1);  /* function to be called */
  c.nresults = nresults;
  status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
  adjustresults(L, nresults);
  lua_unlock(L);
  return status;
}

做了一些检查与准备,之后进入了 luaD_pcall,做完一些错误处理的准备流程(本文暂不涉及)之后又进入了 luaD_rawrunprotected(L, func, u)。目前 func 就是 f_callu 就是 &c,也就是存放了函数指针与返回值数量等信息的 CallS 结构体。

luaD_rawrunprotected 在借助 longjmp 这一机制设置好错误处理函数之后,就直接调用了 f_call

int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
  struct lua_longjmp lj;
  lj.status = 0;
  lj.previous = L->errorJmp;  /* chain new error handler */
  L->errorJmp = &lj;
  LUAI_TRY(L, &lj,
    (*f)(L, ud);
  );
  L->errorJmp = lj.previous;  /* restore old error handler */
  return lj.status;
}

所以我们回到 f_call 函数,此时的参数为 f_call(L, &c)

static void f_call (lua_State *L, void *ud) {
  struct CallS *c = cast(struct CallS *, ud);
  luaD_call(L, c->func, c->nresults);
}

又是一个套娃。由于前边的流程已设置好错误处理机制,这回调用的是不带错误处理的 luaD_call

/*
** Call a function (C or Lua). The function to be called is at *func.
** The arguments are on the stack, right after the function.
** When returns, all the results are on the stack, starting at the original
** function position.
*/ 
void luaD_call (lua_State *L, StkId func, int nResults) {
  // 函数调用栈数量 +1,并判断函数调用栈是不是过长
  if (++L->nCcalls >= LUAI_MAXCCALLS) {
    if (L->nCcalls == LUAI_MAXCCALLS)
      // 爆栈了,进入错误处理流程
      luaG_runerror(L, "C stack overflow");
    else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3)))
      // 爆栈之后,错误处理过程中又爆了
      luaD_throw(L, LUA_ERRERR);  /* error while handing stack error */
  }
  if (luaD_precall(L, func, nResults) == PCRLUA)  /* is a Lua function? */
    luaV_execute(L, 1);  /* call it */
  // 调用完毕, 函数调用栈-1
  L->nCcalls--;
  luaC_checkGC(L);
}

先自增 L->nCcalls,也就是调用栈深度,并检查是否过长。随后又进入了其他函数,分别是 luaD_precallluaV_execute

// 函数调用的预处理, func是函数closure所在位置, nresults是返回值数量
int luaD_precall (lua_State *L, StkId func, int nresults) {
  LClosure *cl;
  ptrdiff_t funcr;
  if (!ttisfunction(func)) /* `func' is not a function? */
    func = tryfuncTM(L, func);  /* check the `function' tag method */
  // 函数指针距离 stack 的偏移量
  funcr = savestack(L, func);
  cl = &clvalue(func)->l;
  // 保存旧的 pc 到当前 ci
  L->ci->savedpc = L->savedpc;
  // Lua 函数的分支
  if (!cl->isC) {  /* Lua function? prepare its call */
    CallInfo *ci;
    StkId st, base;
    Proto *p = cl->p;
    luaD_checkstack(L, p->maxstacksize);
    // 根据 funcr,也就是 func 距离 stack 的偏移量,恢复出 func 指针
    func = restorestack(L, funcr);
    if (!p->is_vararg) {  /* no varargs? */
      base = func + 1;
      if (L->top > base + p->numparams)
        L->top = base + p->numparams;
    }
    else {  /* vararg function */
      int nargs = cast_int(L->top - func) - 1;
      base = adjust_varargs(L, p, nargs);
      func = restorestack(L, funcr);  /* previous call may change the stack */
    }
    // 存放新的 CallInfo 信息
    // 自增 L->ci。如若需要,会执行 growCI(L)
    ci = inc_ci(L);  /* now `enter' new function */
    ci->func = func;
    L->base = ci->base = base;
    ci->top = L->base + p->maxstacksize;
    lua_assert(ci->top <= L->stack_last);
    // 将 pc 指向解析出来的 Proto->code,也就是 opcode 数组
    L->savedpc = p->code;  /* starting point */
    ci->tailcalls = 0;
    ci->nresults = nresults;
    // 清空栈上数据
    for (st = L->top; st < ci->top; st++)
      setnilvalue(st);
    L->top = ci->top;
    if (L->hookmask & LUA_MASKCALL) {
      L->savedpc++;  /* hooks assume 'pc' is already incremented */
      luaD_callhook(L, LUA_HOOKCALL, -1);
      L->savedpc--;  /* correct 'pc' */
    }
    return PCRLUA;
    // 经过这一番处理之后,各个字段的指向如下所示:
    // newci->top = L->top ->   nil     --+
    //                          ...       |  p->maxstacksize
    // (previous) L->top ->     nil       |
    //                          arg     --+
    //                          ...       |  p->numparams
    // newci->base = L->base -> arg     --+
    // newci->func ->           closure
  }
  // 剩下的是 C 函数的分支,就先省略了

经过 luaD_precall 的处理,现在 CallInfo 与栈上结构都已经准备就绪,下一步就直接进入 luaV_execute 了。luaV_execute 是执行 opcode 逻辑的核心函数,对于其详情,详见下一篇文章~

题图来自 Photo by Pavel Neznanov on Unsplash

Lua 虚拟机执行环境的初始化流程》有2个想法

回复 tari 取消回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据