作为下一篇 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_open
、lua_register
、luaL_dofile
与 lua_close
。我们挑出 lua_open
与 luaL_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_loadfile
与 lua_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_call
,u
就是 &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_precall
与 luaV_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
泪目,小圆病倒后还在坚持写文章
得益于优质毒株 =w=