lua-设计与实现-5lua虚拟机

5.1 Lua执行过程概述

一个语言的虚拟机要做的事情:

  1. 编译出字节码
  2. 为函数调用准备调用栈
  3. 维持一个IP(InstructionPointer指令指针)来保存下一个将要执行的指令地址(对应PC指针-OpCode)。
  4. 模拟CPU的运行:循环执行字节码

执行函数

  • 执行Lua文件调用的是luaL_dofile函数————宏来的,调用:luaL_loadfile、lua_pcall
  • luaL_loadfile,词法和语法分析。lua_pcall用于将分析的结果(也就是字节码)放到虚拟机中执行。

luaL_loadfile词法和语法分析

会调用于f_parser函数,词法分析之后产生的字节码等相关数据都在这个Proto类型的结构体中,数据又作为Closure保存了下来,如下

static void f_parser (lua_State *L, void *ud) {
  int i;
  Proto *tf;
  Closure *cl;
...
  tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z,
                                                             &p->buff, p->name);
  cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
  cl->l.p = tf;
...
  setclvalue(L, L->top, cl);
  incr_top(L);
}

//真正的词法语法分析 -- 分析一个lua源代码文件的主函数
Proto *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, const char *name) {
  struct LexState lexstate;
  struct FuncState funcstate;
  lexstate.buff = buff;
  luaX_setinput(L, &lexstate, z, luaS_new(L, name));
  open_func(&lexstate, &funcstate);
  // 这是为什么呢?
  funcstate.f->is_vararg = VARARG_ISVARARG;  /* main func. is always vararg */
  // 读入字符
  luaX_next(&lexstate);  /* read first token */
  chunk(&lexstate);
  check(&lexstate, TK_EOS);
  close_func(&lexstate);
  lua_assert(funcstate.prev == NULL);
  lua_assert(funcstate.f->nups == 0);
  lua_assert(lexstate.fs == NULL);
  return funcstate.f;
}


lua_pcall字节码的执行

LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) {
  ...
  c.func = L->top - (nargs+1);  /* function to be called */
  ...
  status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
  ...
  return status;
}

以上源码解读:

  • c.func函数对象指针就是前面f_parser函数中最后两句代码放入Lua梢的Closure指针
int luaD_precall (lua_State *L, StkId func, int nresults) {
      ...
       Proto *p = cl->p;
      ...
    // 存放新的函数信息
    // 首先从callinfo数组中分配出一个新的callinfo
    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);
    // 改变代码执行的路径
    L->savedpc = p->code;  /* starting point */
      
    for (st = L->top; st < ci->top; st++)
      setnilvalue(st);
}

以上源码解读:

  • 从lua _State 的Call Info 数组中得到一个新的Call Info结构体,设置它的func、base 、top指针。
  • 第2 75行用于从前面分析阶段生成的Closure指针中,取出保存下来的Proto结构体。前面提到过,这个结构体中保存的是分析过程完结之后生成的字节码等信息。
  • 将这里创建的Call Info指针的top/base指针赋值给lua_State结构体的top 、base 指针。第293 行将Proto 结构体的code成员赋值给lua_State 指针的saved pc字段, code 成员保留的就是字节码。
  • 第296 ~297行的作用是把多余的函数参数赋值为nil ;比如一个函数定
void luaV_execute (lua_State *L, int nexeccalls) {
  ...
  pc = L->savedpc;
  ...
  /* main loop of interpreter */
  for (;;) {
    const Instruction i = *pc++;
    ...
       case OP_RETURN: {
        ...
        b = luaD_poscall(L, ra); //执行完毕后,调用luaD_poscall 函数恢复到上一个函数的环境。
  }
}

以上源码解读:

  • 就是大循环。 执行完毕后,还会调用luaD_poscall 函数恢复到上一次函数调用的环境:

大致的流程回顾:
(1)在飞parser 函数中,对代码文件的分析返回了Proto指针。这个指针会保存在Closure 指针中,留待后续继续使用。
(2)在luaD_precall 函数中,将lua_state 的saved pc 指针指向第1 步中Proto 结构体的code指针,同时准备好函数调用时的战信息。
(3)在luaV_execute 函数中, pc 指针指向第2步中的saved pc 指针,紧眼着就是一个大的循环体,依次取出其中的OpCode执行。

  • Proto结构体是分析阶段和执行阶段的纽带(如图5- 5所示)。只要抓住了Proto 结构体这一个数据的流向,就能对从分析到执行的整个流程有大体的了解了。

Proto结构体(部分):

  • 函数的常量数组;
  • 编译生成的字节码信息,也就是前面提到的code成员;
  • 函数的局部变量信息;
  • 保存upvalue 名字的数组。

5.2数据结构与栈

每个Lua 虚拟机对应一个lua State结构体,它使用TValue 数组来模拟栈,

  • stack :栈数组的起始位置。
  • base : 当前函数栈的基地址。 ((lvm.c) 343 #define RA(i) (base+GETARG_A(i)))
  • top : 当前栈的下一个可用位置。

无论函数怎么执行,有多少函数,最终它们引用到的楼都是当前Lua虚拟机的拢。这好比一个操作系统中的进程无论有多少,最终引用的内存实际上都还是由操作系统内核来管理的。

lua_State 结构体与Callinfo 结构体之间是如何对应的呢?

在lua_State中,有一个base_ci 的CallInfo数组,存储的就是CallInfo 的信息。而另一个ci成员,指向的就是当前函数的CallInfo指针。
可以看到, lua_State结构体中的top 、base 指针是与函数执行相关的变量,在函数执行前后都会有所变化。

从图5-6和图5-7 中可以看到,两个函数执行期间CallInfo指针分别指向lua_State指针分配的栈数组的不同位置。而随着当前函数的变化, lua_State结构体中的top和base 指针的指向也发生了变化,这两个变量始终指向当前执行函数的对应位置。

  • 前后调用的函数中Lua梭的大小是有限的,同时Call Info数组的大小也是有限的 械的使用和函数的嵌套层次都不能过多,以防这些资源、用尽了
    (只有函数嵌套的时候,函数CallInfollInfo数组里才有多个吗??)

5.3 指令的解析

  • 词法、语法阶段的分析中,最后结果就是输出一个Proto 结构体
  • FuncState:这个结构体用于在语法分析时保存解析函数之后相关的信息,根据其中的prev指针成员来串联起来
  • FuncState有一个成员Proto 叫,它用来保存这个FuncState解析指令之后生成的指令,其中除了自己的,还包括内部嵌套的子函数的。

5.4 指令格式


规则:

  • 低位向高位解读
    举例:
    OP_MOVE A B R(A) :=R(B) 从R(B)中取数据赋值给R(A)
    OP_GETGLOBAL A Bx R(A) := Gbl[Kst(Bx)) 以Kst[Bx]作为全局符号表的索引,取出值后赋值给R(A)
    OP_GETIABLE A B C R(A) := R(B)[RK(C)) 以RK(C)作为表索引,以R(B)的数据作为表,取出来的数据赋值
    给R(A)
posted @ 2020-07-16 11:38  天山鸟  阅读(597)  评论(0编辑  收藏  举报