zoukankan      html  css  js  c++  java
  • 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)
  • 相关阅读:
    代码1
    js中级第13天
    dom 浏览器模型
    js中级第12天
    js中级第11天
    js中级第十天
    js中级第九天
    js中级第8天
    js中级第六天
    js中级第七天
  • 原文地址:https://www.cnblogs.com/Jaysonhome/p/13321812.html
Copyright © 2011-2022 走看看