首页 > lua, 源码阅读 > Lua源码剖析(四)

Lua源码剖析(四)

原创文章,转载请注明: 转载自pagefault

本文链接地址: Lua源码剖析(四)

前面三篇请看我前面的 blog

这篇主要来分析lua的虚拟机的实现,我看的代码依旧是5.1

因此首先从luaL_loadfile开始,这个函数我们知道是在当前的lua state加载一个lua文件,其中第二个参数就是filename。

其中LoadF结构很简单,它用来表示一个load file:

typedef struct LoadF {
  int extraline;
  FILE *f;
  char buff[LUAL_BUFFERSIZE];
} LoadF;

其中会使用fopen来打开对应的文件名,然后根据第一个字符来判断是否是注释(#),如果是则跳过

    lua_pushfstring(L, "@%s", filename);
    lf.f = fopen(filename, "r");
    if (lf.f == NULL) return errfile(L, "open", fnameindex);
  }
  c = getc(lf.f);
  if (c == '#') {  /* Unix exec. file? */
    lf.extraline = 1;
    while ((c = getc(lf.f)) != EOF && c != '\n') ;  /* skip first line */
    if (c == '\n') c = getc(lf.f);
  }


然后会判断是否是lua字节码文件,通过文件头的magic number(Lua),如果是,则用二进制打开。

  if (c == LUA_SIGNATURE[0] && filename) {  /* binary file? */
    lf.f = freopen(filename, "rb", lf.f);  /* reopen in binary mode */
    if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
    /* skip eventual `#!...' */
   while ((c = getc(lf.f)) != EOF && c != LUA_SIGNATURE[0]) ;
    lf.extraline = 0;
  }

最后就是调用lua_load来load文件。这里有一个结构要注意,那就是zio,这个结构就是一个parse的结构。这里的reader就是一个回调函数。

struct Zio {
  size_t n;               /* bytes still unread */
  const char *p;          /* current position in buffer */
  lua_Reader reader;
  void* data;               /* additional data */
  lua_State *L;               /* Lua state (for reader) */
};

可以看看这个结构如何被初始化。

void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, void *data) {
  z->L = L;
  z->reader = reader;
  z->data = data;
  z->n = 0;
  z->p = NULL;
}

先看看lua的BNF

chunk ::= {stat [`;´]} [laststat [`;´]] block ::= chunk stat ::= varlist `=´ explist | functioncall | do block end | while exp do block end | repeat block until exp | if exp then block {elseif exp then block} [else block] end | for Name `=´ exp `,´ exp [`,´ exp] do block end | for namelist in explist do block end | function funcname funcbody | local function Name funcbody | local namelist [`=´ explist] laststat ::= return [explist] | break funcname ::= Name {`.´ Name} [`:´ Name] varlist ::= var {`,´ var} var ::= Name | prefixexp `[´ exp `]´ | prefixexp `.´ Name namelist ::= Name {`,´ Name} explist ::= {exp `,´} exp exp ::= nil | false | true | Number | String | `…´ | function | prefixexp | tableconstructor | exp binop exp | unop exp prefixexp ::= var | functioncall | `(´ exp `)´ functioncall ::= prefixexp args | prefixexp `:´ Name args args ::= `(´ [explist] `)´ | tableconstructor | String function ::= function funcbody funcbody ::= `(´ [parlist] `)´ block end parlist ::= namelist [`,´ `…´] | `…´ tableconstructor ::= `{´ [fieldlist] `}´ fieldlist ::= field {fieldsep field} [fieldsep] field ::= `[´ exp `]´ `=´ exp | Name `=´ exp | exp fieldsep ::= `,´ | `;´ binop ::= `+´ | `-´ | `*´ | `/´ | `^´ | `%´ | `..´ | `<´ | `<=´ | `]]>´ | `>=´ | `==´ | `~=´ | and | or unop ::= `-´ | not | `#´

可以看到reader就是被初始化为getF,而data是被初始化位上面的LoadF结构,然后就是调用luaD_protectedparser来parse源代码。
在初始化的时候调用luaX_init将保留关键字作为字符串放入全局的string hash中(ts->reserved),因此当解析到字符串时,能够很容易的到当前字符串的类型。
解析函数是luaY_parser, 而核心的parse方法是llex,这个函数会返回对应的token,然后根据解析出来的token,来决定接下来理应是什么符号(核心在chunk函数里面,这函数有一个循环), 然后每次都会调用luaX_next函数,来继续解析(它会调用llex).

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;
}

可以看到最终luaY_parser返回的是一个proto,也就是说每一个lua文件也就相当于一个proto(函数).

下面湿核心的chunk方法.

static void chunk (LexState *ls) {
  /* chunk -> { stat [`;'] } */
  int islast = 0;
  enterlevel(ls);
  while (!islast && !block_follow(ls->t.token)) {
    islast = statement(ls);
    testnext(ls, ';');
    lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg &&
               ls->fs->freereg >= ls->fs->nactvar);
    ls->fs->freereg = ls->fs->nactvar;  /* free registers */
  }
  leavelevel(ls);
}

lua里面token定义为:

enum RESERVED {
  /* terminal symbols denoted by reserved words */
  TK_AND = FIRST_RESERVED, TK_BREAK,
  TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION,
  TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT,
  TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE,
  /* other terminal symbols */
  TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE, TK_NUMBER,
  TK_NAME, TK_STRING, TK_EOS
}

可以看到上方是关键字,而下方是其他的一些终结符。
核心的parse结构LexState,它主要是用于parse期间保存状态。

typedef struct LexState {
  int current;  /* current character (charint) */
  int linenumber;  /* input line counter */
  int lastline;  /* line of last token `consumed' */
  Token t;  /* current token */
//表示前一个token
  Token lookahead;  /* look ahead token */
  struct FuncState *fs;  /* `FuncState' is private to the parser */
  struct lua_State *L;
  ZIO *z;  /* input stream */
  Mbuffer *buff;  /* buffer for tokens */
  TString *source;  /* current source name */
  char decpoint;  /* locale decimal point */
} LexState;

上面这个结构中最需要注意的是Token结构,这个结构保存了对应的解析出来的token。seminfo保存了经过词法解析后的词(不包括运算符以及[]等),只包括字母以及数字number and string。在lua中所有的tiken分为13类,也就是BNF中的binop(不包括and or)+各种关键字。

typedef union {
  lua_Number r;
  TString *ts;
} SemInfo;  /* semantics information */


typedef struct Token {
  int token;
  SemInfo seminfo;
} Token;

然后就是每个解析出来的函数的数据结构,这个结构保存了解析到的函数的状态。

typedef struct FuncState {
  Proto *f;  /* current function header */
  Table *h;  /* table to find (and reuse) elements in `k' */
  struct FuncState *prev;  /* enclosing function */
  struct LexState *ls;  /* lexical state */
  struct lua_State *L;  /* copy of the Lua state */
  struct BlockCnt *bl;  /* chain of current blocks */
  int pc;  /* next position to code (equivalent to `ncode') */
  int lasttarget;   /* `pc' of last `jump target' */
  int jpc;  /* list of pending jumps to `pc' */
  int freereg;  /* first free register */
  int nk;  /* number of elements in `k' */
  int np;  /* number of elements in `p' */
  short nlocvars;  /* number of elements in `locvars' */
  lu_byte nactvar;  /* number of active local variables */
  upvaldesc upvalues[LUAI_MAXUPVALUES];  /* upvalues */
  unsigned short actvar[LUAI_MAXVARS];  /* declared-variable stack */
} FuncState;

上面这几个结构的初始化都是放在luaY_parser中的,llex方法其实只是一个词法扫描器,它只负责扫描符号以及单词,然后它会将扫描到的符号交给后续的语法分析器去判断。
其中luaX_setinput用于初始化设置lexState的一些输入属性。

void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source) {
  ls->decpoint = '.';
  ls->L = L;
//前一个token
  ls->lookahead.token = TK_EOS;  /* no look-ahead token */
  ls->z = z;
  ls->fs = NULL;
  ls->linenumber = 1;
  ls->lastline = 1;
  ls->source = source;
  luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER);  /* initialize buffer */
//第一次读取token
  next(ls);  /* read first char */
}

而open_func函数则是新建一个func对象.

static void open_func (LexState *ls, FuncState *fs) {
  lua_State *L = ls->L;
  Proto *f = luaF_newproto(L);
  fs->f = f;
  fs->prev = ls->fs;  /* linked list of funcstates */
  fs->ls = ls;
  fs->L = L;
  ls->fs = fs;
  fs->pc = 0;
  fs->lasttarget = -1;
  fs->jpc = NO_JUMP;
  fs->freereg = 0;
  fs->nk = 0;
  fs->np = 0;
  fs->nlocvars = 0;
  fs->nactvar = 0;
  fs->bl = NULL;
  f->source = ls->source;
  f->maxstacksize = 2;  /* registers 0/1 are always valid */
  fs->h = luaH_new(L, 0, 0);
  /* anchor table of constants and prototype (to avoid being collected) */
  sethvalue2s(L, L->top, fs->h);
  incr_top(L);
  setptvalue2s(L, L->top, f);
  incr_top(L);
}

然后会在statement函数中对语句进行语法解析,然后不同的函数解析不同的语句。
assignment函数用于解析赋值语法,而它会调用exploits1,explist1对应的解析 explist1 -> expr { `,’ expr } ,因此在它里面就会调用expr函数,而在expr中是直接调用subexpr函数,它主要是解析这类语法subexpr -> (simpleexp | unop subexpr) { binop subexpr } . 因此在subexpr中就会调用三个函数,分别是simpleexp (simpleexp -> NUMBER | STRING | NIL | true | false | … | constructor | FUNCTION body | primaryexp), getunopr来解析一元操作符,getbinopr来解析二元操作符。 在simpleexp中,则会调用primaryexp来解析 primaryexp -> prefixexp { `.’ NAME | `[‘ exp `]’ | `:’ NAME funcargs | funcargs } 这类语法。

每次使用checknext方法来检测下一个token是否是期望的。

static void check (LexState *ls, int c) {
  if (ls->t.token != c)
    error_expected(ls, c);
}

static void checknext (LexState *ls, int c) {
  check(ls, c);
  luaX_next(ls);
}

lua的自顶向下的解析最核心的就是lcode.c 和lparse.c.其中lua对待一个文件就是把这个文件当做一个函数,因此在解析完毕之后,返回的就是一个proto指针。对应的lparse是解析lua源代码,而lcode.c则将对应的源码翻译为虚拟机指令(保存在proto的code数组中).

在lua中每一个表达式解析完毕后就表示为一个这样的结构体:

typedef struct expdesc {
  expkind k;
  union {
    struct { int info, aux; } s;
    lua_Number nval;
  } u;
  int t; /* patch list of `exit when true' */
  int f; /* patch list of `exit when false' */
} expdesc;

这里最重要的就是联合体u,其中info表示语句对应的索引(在指令数组code(proto结构体)中).或者说表示为对应constant 的索引(proto的TValue *k这个数组的索引).这就体现了程序是由数据+指令组成。

lua的虚拟机指令的格式:

/*===========================================================================
  We assume that instructions are unsigned numbers.
  All instructions have an opcode in the first 6 bits.
  Instructions can have the following fields:
 `A' : 8 bits
 `B' : 9 bits
 `C' : 9 bits
 `Bx' : 18 bits (`B' and `C' together)
 `sBx' : signed Bx

  A signed argument is represented in excess K; that is, the number
  value is the unsigned value minus K. K is exactly the maximum value
  for that argument (so that -max is represented by 0, and +max is
  represented by 2*max), which is half the maximum for the corresponding
  unsigned argument.
===========================================================================*/
enum OpMode {iABC, iABx, iAsBx}; /* basic instruction format */

可以看到也就是3种指令,而下面的宏定义了指令的参数以及opcode的大小以及偏移,这里可以看到在lua里面所有的指令都是定长的。

#define SIZE_C9
#define SIZE_B9
#define SIZE_Bx(SIZE_C + SIZE_B)
#define SIZE_A8

#define SIZE_OP6

#define POS_OP0
#define POS_A(POS_OP + SIZE_OP)
#define POS_C(POS_A + SIZE_A)
#define POS_B(POS_C + SIZE_C)
#define POS_BxPOS_C

再接下来就是如何取得对应的opcode以及各个参数:

/* creates a mask with `n' 1 bits at position `p' */
#define MASK1(n,p)((~((~(Instruction)0)<<n))<<p)

/* creates a mask with `n' 0 bits at position `p' */
#define MASK0(n,p)(~MASK1(n,p))

/*
** the following macros help to manipulate instructions
*/

#define GET_OPCODE(i)(cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0)))
#define SET_OPCODE(i,o)((i) = (((i)&MASK0(SIZE_OP,POS_OP)) | \
  ((cast(Instruction, o)<<POS_OP)&MASK1(SIZE_OP,POS_OP))))

对应的上面就是如何取得对应指令 opcode的宏。下面就是所有的opcode类型。

typedef enum {
/*----------------------------------------------------------------------
nameargsdescription
------------------------------------------------------------------------*/
OP_MOVE,/*A BR(A) := R(B)*/
OP_LOADK,/*A BxR(A) := Kst(Bx)*/
OP_LOADBOOL,/*A B CR(A) := (Bool)B; if (C) pc++*/
OP_LOADNIL,/*A BR(A) := ... := R(B) := nil*/
OP_GETUPVAL,/*A BR(A) := UpValue[B]*/

OP_GETGLOBAL,/*A BxR(A) := Gbl[Kst(Bx)]*/
OP_GETTABLE,/*A B CR(A) := R(B)[RK(C)]*/

OP_SETGLOBAL,/*A BxGbl[Kst(Bx)] := R(A)*/
OP_SETUPVAL,/*A BUpValue[B] := R(A)*/
OP_SETTABLE,/*A B CR(A)[RK(B)] := RK©*/

OP_NEWTABLE,/*A B CR(A) := {} (size = B,C)*/

OP_SELF,/*A B CR(A+1) := R(B); R(A) := R(B)[RK(C)]*/

OP_ADD,/*A B CR(A) := RK(B) + RK©*/
OP_SUB,/*A B CR(A) := RK(B) - RK©*/
OP_MUL,/*A B CR(A) := RK(B) * RK©*/
OP_DIV,/*A B CR(A) := RK(B) / RK©*/
OP_MOD,/*A B CR(A) := RK(B) % RK©*/
OP_POW,/*A B CR(A) := RK(B) ^ RK©*/
OP_UNM,/*A BR(A) := -R(B)*/
OP_NOT,/*A BR(A) := not R(B)*/
OP_LEN,/*A BR(A) := length of R(B)*/

OP_CONCAT,/*A B CR(A) := R(B).. ... ..R©*/

OP_JMP,/*sBxpc+=sBx*/

OP_EQ,/*A B Cif ((RK(B) == RK(C)) ~= A) then pc++*/
OP_LT,/*A B Cif ((RK(B) < RK(C)) ~= A) then pc++ */
OP_LE,/*A B Cif ((RK(B) <= RK(C)) ~= A) then pc++ */

OP_TEST,/*A Cif not (R(A) <=> C) then pc++*/ 
OP_TESTSET,/*A B Cif (R(B) <=> C) then R(A) := R(B) else pc++*/ 

OP_CALL,/*A B CR(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
OP_TAILCALL,/*A B Creturn R(A)(R(A+1), ... ,R(A+B-1))*/
OP_RETURN,/*A Breturn R(A), ... ,R(A+B-2)(see note)*/

OP_FORLOOP,/*A sBxR(A)+=R(A+2);
   if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/*A sBxR(A)-=R(A+2); pc+=sBx*/

OP_TFORLOOP,/*A CR(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2)); 
                        if R(A+3) ~= nil then R(A+2)=R(A+3) else pc++*/ 
OP_SETLIST,/*A B CR(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B*/

OP_CLOSE,/*A close all variables in the stack up to (>=) R(A)*/
OP_CLOSURE,/*A BxR(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n))*/

OP_VARARG/*A BR(A), R(A+1), ..., R(A+B-1) = vararg*/
} OpCode;

然后我们来看一个最简单的语句是如何解析进lua的虚拟机的,假设有一条 local t = 3 的语句。这个会通过parse进入localstat,然后调用new_localvar来新建一个local变量,所有的local变量都放在Proto结构的f->locvars中,locvars也就是一个数组

struct LocVar *locvars; /* information about local variables */

而对应的数组索引放到FuncState的actvar数组中。因为在lua中可以这么写local t, t1 = 3。解析完毕名字之后,就开始解析=,解析=会在explist1函数中进行。然后会讲解析掉的数字3 放入到表达式(expdesc)的nval域。

typedef struct expdesc {
  expkind k;
  union {
    struct { int info, aux; } s;
    lua_Number nval;
  } u;
  int t; /* patch list of `exit when true' */
  int f; /* patch list of `exit when false' */
} expdesc;

    case TK_NUMBER: {
      init_exp(v, VKNUM, 0);
      v->u.nval = ls->t.seminfo.r;
      break;
    }

这里可以看到直接赋值给nval。

然后就是adjust_assign以及adjustlocalvars两个函数,其中adjust_assign将会调用luaK_exp2nextreg函数,而在这个函数中则会调用discharge2reg函数来设置对应的指令, 这里比较关键的,就是lua如何来选择寄存器。


void luaK_reserveregs (FuncState *fs, int n) {
  luaK_checkstack(fs, n);
  fs->freereg += n;
}

void luaK_exp2nextreg (FuncState *fs, expdesc *e) {
  luaK_dischargevars(fs, e);
  freeexp(fs, e);
  luaK_reserveregs(fs, 1);
  exp2reg(fs, e, fs->freereg - 1);
}

static void exp2reg (FuncState *fs, expdesc *e, int reg) {
  discharge2reg(fs, e, reg);
..............
}

static void discharge2reg (FuncState *fs, expdesc *e, int reg) {
  luaK_dischargevars(fs, e);
..........................
    case VKNUM: {
      luaK_codeABx(fs, OP_LOADK, reg, luaK_numberK(fs, e->u.nval));
      break;
    }
..........................
  e->u.s.info = reg;
  e->k = VNONRELOC;
}

通过上面我们可以看到寄存器的选择是通过funcstate的freereg域来进行选择的。最终我们可以看到当字节码生成之后,e->u.s.info中放的就是寄存器了。而在一开始进入luaK_exp2nextreg之后,立即对freereg进行+1,这里寄存器我们可以看做是一个索引.

接下来我们来看luaK_numberK的实现,也就是lua会把constant放到哪里去。

int luaK_numberK (FuncState *fs, lua_Number r) {
  TValue o;
  setnvalue(&o, r);
  return addk(fs, &o, &o);
}


static int addk (FuncState *fs, TValue *k, TValue *v) {
  lua_State *L = fs->L;
  TValue *idx = luaH_set(L, fs->h, k);
  Proto *f = fs->f;
  int oldsize = f->sizek;
  if (ttisnumber(idx)) {
    lua_assert(luaO_rawequalObj(&fs->f->k[cast_int(nvalue(idx))], v));
    return cast_int(nvalue(idx));
  }
  else {  /* constant not found; create a new entry */
    setnvalue(idx, cast_num(fs->nk));
    luaM_growvector(L, f->k, fs->nk, f->sizek, TValue,
                    MAXARG_Bx, "constant table overflow");
    while (oldsize < f->sizek) setnilvalue(&f->k[oldsize++]);
    //存储值
    setobj(L, &f->k[fs->nk], v);
    luaC_barrier(L, f, v);
    return fs->nk++;
  }
}

可以看到最终值会放到f->k这个数组中。并且会返回对应的索引,然后讲索引保存到字节码中。

这个时候可以看到这条语句对应的字节码是LOADK, 而loadk对应的指令类型是ABx,我们来看对应的域都填充的是什么。

int luaK_codeABx (FuncState *fs, OpCode o, int a, unsigned int bc) {
  lua_assert(getOpMode(o) == iABx || getOpMode(o) == iAsBx);
  lua_assert(getCMode(o) == OpArgN);
  return luaK_code(fs, CREATE_ABx(o, a, bc), fs->ls->lastline);
}

可以看到对应的a填充到ABx类型的A,而bc则填充到bx域,而a是什么呢?通过上面的代码可以看到a就是寄存器,bc就是对应的值.也就是说讲3这个值放到bx,而把t对应的寄存器 放到a域.字节码生成了。

然后看来luaK_code这个函数,他也就是讲指令放入到proto结构体的code数组中

static int luaK_code (FuncState *fs, Instruction i, int line) {
  Proto *f = fs->f;
  dischargejpc(fs);  /* `pc' will change */
  /* put new instruction in code array */
  luaM_growvector(fs->L, f->code, fs->pc, f->sizecode, Instruction,
                  MAX_INT, "code size overflow");
  f->code[fs->pc] = i;
  /* save corresponding line information */
  luaM_growvector(fs->L, f->lineinfo, fs->pc, f->sizelineinfo, int,
                  MAX_INT, "code size overflow");
  f->lineinfo[fs->pc] = line;
  return fs->pc++;
}

接下来我们来看lua虚拟机会如何来解析字节码。

所有的解析都在luaV_execute中。来看对应的代码, luaV_execute也就是会通过遍历L->code(索引在savedpc中)来执行所有的字节码。


void luaV_execute (lua_State *L) {
  CallInfo *ci = L->ci;
.................

  pc = L->savedpc;
  cl = &clvalue(L->ci->func)->l;
  base = L->base;
//取出constants数组
  k = cl->p->k;
  /* main loop of interpreter */
  for (;;) {
    const Instruction i = *pc++;
    StkId ra;
    if ((L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) &&
        (--L->hookcount == 0 || L->hookmask & LUA_MASKLINE)) {
      traceexec(L, pc);
      if (L->status == LUA_YIELD) {  /* did hook yield? */
        L->savedpc = pc - 1;
        return;
      }
      base = L->base;
    }
.....................
    ra = RA(i);
.............
      case OP_LOADK: {
        setobj2s(L, ra, KBx(i));
        continue;
      }
    }

这里ra就是对应的栈的位置(通过寄存器来确定栈的位置), 然后KBx主要是用于得到对应的值(也就是我们例子中的5).

#define KBx(i)	check_exp(getBMode(GET_OPCODE(i)) == OpArgK, k+GETARG_Bx(i))

可以看到他是以k = cl->p->k;为基准值,然后加上对应的偏移.

最后我们来看setobj2s这个函数,这个函数其实很简单,就是把从寄存器取出来的值放入到对应的value结构中。

#define setobj(L,obj1,obj2) \
  { const TValue *o2=(obj2); TValue *o1=(obj1); \
    o1->value = o2->value; o1->tt=o2->tt; \
    checkliveness(G(L),o1); }

可以看到最终t的值被放入到L->base为基础的一段内存中。

Share
分类: lua, 源码阅读 标签:
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.