目录

一、虚拟机篇 - 编译过程的核心数据结构

二、虚拟机篇 - 指令集存储结构Instruction

三、虚拟机篇 - statlist状态机实现

四、虚拟机篇 - 通过IF语句示例看执行过程


上一章节,讲解了语法的解析功能luaX_next,这一章节主要讲解虚拟机代码编译成操作码的过程。

一、虚拟机篇 - 编译过程的核心数据结构


我们首先看下,Lua核心虚拟机实现的几个重要文件:llex.c 语义分割器、lparse.c 语法树解析器、lcode.c 可执行代码生成

整个Lua代码编译的过程,主要在lparse.c文件中实现,入口函数为:luaY_parser 。

Lua的代码是一边解析,一边编译,生成二进制字节码指令Opcode的,Opcode会放置在FuncState->Proto结构中的code数组上

Lua文件的解析通过LexState结构来管理整体的语法解析状态;语法块和函数的编译,则通过FuncState结构来管理;编译出来的二进制指令集,则保存在Proto结构中的code数组上:

  • LexState:语法分析上下文状态。该结构贯穿编译过程的始终,主要存储解析和编译过程中的语法树状态。

  • FuncState:函数编译状态。该结构主要存储语法块、函数等编译过程状态信息。

  • Proto:主要存放二进制的字节码指令集(Opcode)。字节码指令集主要存储在code的数组上;FuncState->pc指向下一个code的地址。Proto结构挂载在FuncState函数栈状态结构上。

/*
 * 语法分析上下文状态
 * state of the lexer plus state of the parser when shared by all
   functions
    */
typedef struct LexState {
  int current;  /* 解析字符指针 current character (charint) */
  int linenumber;  /* 行数计数器 input line counter */
  int lastline;  /* 最后一行 line of last token 'consumed' */
  Token t;  /* 当前Token current token */
  Token lookahead;  /* 头部Token look ahead token */
  struct FuncState *fs;  /* 当前解析的方法 current function (parser) */
  struct lua_State *L; //Lua栈
  ZIO *z;  /* io输入流 input stream */
  Mbuffer *buff;  /* buffer for tokens */
  Table *h;  /* to avoid collection/reuse strings */
  struct Dyndata *dyd;  /* dynamic structures used by the parser */
  TString *source;  /* 当前源名称 current source name */
  TString *envn;  /* 环境变量 environment variable name */
} LexState;

/**
 * 存储函数编译状态
 */
typedef struct FuncState {
  Proto *f;  /* 存放Opcode current function header */
  struct FuncState *prev;  /* enclosing function */
  struct LexState *ls;  /* 词法状态 lexical state */
  struct BlockCnt *bl;  /* 当前块链 chain of current blocks */
  int pc;  /* 代码的下一个位置,指向Proto->code中的数组指针 next position to code (equivalent to 'ncode') */
  int lasttarget;   /* 'label' of last 'jump label' */
  int jpc;  /* 即将跳转的pc列表 list of pending jumps to 'pc' */
  int nk;  /* number of elements in 'k' */
  int np;  /* number of elements in 'p' */
  int firstlocal;  /* index of first local var (in Dyndata array) */
  short nlocvars;  /* number of elements in 'f->locvars' */
  lu_byte nactvar;  /* number of active local variables */
  lu_byte nups;  /* number of upvalues */
  lu_byte freereg;  /* first free register */
} FuncState;

/*
** Function Prototypes
** Proto主要存放二进制指令集Opcode
** Lua在解析函数的过程中,会将一条条语句逐个‘编译’成指令集
*/
typedef struct Proto {
  CommonHeader;
  lu_byte numparams;  /* 定参个数 number of fixed parameters */
  lu_byte is_vararg;
  lu_byte maxstacksize;  /* 栈个数 number of registers needed by this function */
  int sizeupvalues;  /* size of 'upvalues' */
  int sizek;  /* size of 'k' */
  int sizecode; //code个数
  int sizelineinfo;
  int sizep;  /* size of 'p' */
  int sizelocvars;
  int linedefined;  /* debug information  */
  int lastlinedefined;  /* debug information  */
  TValue *k;  /* 常量表 constants used by the function */
  Instruction *code;  /* 存储指令集数组 opcodes */
  struct Proto **p;  /* functions defined inside the function */
  int *lineinfo;  /* map from opcodes to source lines (debug information) */
  LocVar *locvars;  /* information about local variables (debug information) */
  Upvaldesc *upvalues;  /* upvalue information */
  struct LClosure *cache;  /* last-created closure with this prototype */
  TString  *source;  /* used for debug information */
  GCObject *gclist;
} Proto;

二、虚拟机篇 - 指令集存储结构Instruction


Proto中的code主要是存储字节码指令集的数组。code的类型是Instruction,而Instruction在宏定义中是一个32位的unsigned int类型。

其中,前面6位放置Opcode操作指令,8位放置操作指令A,9位放置操作指令B,9位放置操作指令C

在lcode.c中,我们可以找到luaK_codeABC函数,主要封装了指令的生成函数。CREATE_ABC定义了宏生成操作指令。luaK_code函数将指令设置到Proto->code上。

/*
** type for virtual-machine instructions;
** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h)
*/
#if LUAI_BITSINT >= 32
typedef unsigned int Instruction;
#else
typedef unsigned long Instruction;
#endif

#define CREATE_ABC(o,a,b,c)	((cast(Instruction, o)<<POS_OP) \
			| (cast(Instruction, a)<<POS_A) \
			| (cast(Instruction, b)<<POS_B) \
			| (cast(Instruction, c)<<POS_C))

/*
** Format and emit an 'iABC' instruction. (Assertions check consistency
** of parameters versus opcode.)
*/
int luaK_codeABC (FuncState *fs, OpCode o, int a, int b, int c) {
  lua_assert(getOpMode(o) == iABC);
  lua_assert(getBMode(o) != OpArgN || b == 0);
  lua_assert(getCMode(o) != OpArgN || c == 0);
  lua_assert(a <= MAXARG_A && b <= MAXARG_B && c <= MAXARG_C);
  return luaK_code(fs, CREATE_ABC(o, a, b, c));
}

/*
** Emit instruction 'i', checking for array sizes and saving also its
** line information. Return 'i' position.
** Opcode存放在Proto结构上
** 其中f->code数组用于存放code
** fs->pc主要是计数器,标记code的个数及数组下标
*/
static int luaK_code (FuncState *fs, Instruction i) {
  Proto *f = fs->f;
  dischargejpc(fs);  /* 'pc' will change */
  /* put new instruction in code array */
  luaM_growvector(fs->ls->L, f->code, fs->pc, f->sizecode, Instruction,
                  MAX_INT, "opcodes");
  f->code[fs->pc] = i;
  /* save corresponding line information */
  luaM_growvector(fs->ls->L, f->lineinfo, fs->pc, f->sizelineinfo, int,
                  MAX_INT, "opcodes");
  f->lineinfo[fs->pc] = fs->ls->lastline;
  return fs->pc++;
}

三、虚拟机篇 - statlist状态机实现


Lua语言解析和编译过程的入口函数是luaY_parser,而真正实现状态机的是在mainfunc函数中的statlist。

statlist函数,主要通过while语句实现状态机的循环。通过luaX_next,逐个切割出语义Token,通过状态机循环,将语义转化成语法块,并逐个编译成二进制可执行指令Opcode。

statlist有几个关键要点:

  • 状态机是通过语法块,然后逐块将代码解析成Opcode的。代码块如:if语句、for循环、function函数、表达式等。
  • 语法块的判断是根据ls->t.token Token的值来确定的,根据不同的Token确定不同语法块的起始位置。
  • 状态机中也会调用luaX_next函数,不断切割Lua的语法Token,并在状态机中进行解析 + 编译。
  • 语法块中,经常会遇到嵌套的语法块,这个时候会回调statlist函数,进行递归遍历

statlist函数中,只有遇到return标识的时候,状态机循环才会中断,否则会持续运行到语法解析编译完毕。

/**
 * 语法树解析
 */
static void statlist (LexState *ls) {
  /* statlist -> { stat [';'] } */
  while (!block_follow(ls, 1)) {
    if (ls->t.token == TK_RETURN) {
      statement(ls);
      return;  /* 最后一个语法块 'return' must be last statement */
    }
    statement(ls);
  }
}

statement函数主要状态机的执行函数。通过switch方法,针对不同的Token值,执行不同的语法块处理。

Lua语言是有作用域的概念的。所以进入一个语法块的时候,会执行enterlevel,离开一个语法块的时候,调用leavelevel函数。

如果命中了某一个语法块,则进入对应的语法块处理逻辑(例如ifstat);默认情况下,会进入表达式的处理流程exprstat。

/**
 * 解析语法树,按照块状分割
 */
static void statement (LexState *ls) {
  int line = ls->linenumber;  /* may be needed for error messages */
  enterlevel(ls); //作用域
  switch (ls->t.token) {
    case ';': {  /* stat -> ';' (empty statement) */
      luaX_next(ls);  /* skip ';' */
      break;
    }
    case TK_IF: {  /* stat -> ifstat */
      ifstat(ls, line);
      break;
    }
    case TK_WHILE: {  /* stat -> whilestat */
      whilestat(ls, line);
      break;
    }
    ......
    case TK_RETURN: {  /* stat -> retstat */
      luaX_next(ls);  /* skip RETURN */
      retstat(ls);
      break;
    }
    case TK_BREAK:   /* stat -> breakstat */
    case TK_GOTO: {  /* stat -> 'goto' NAME */
      gotostat(ls, luaK_jump(ls->fs));
      break;
    }
    //表达式处理
    default: {  /* stat -> func | assignment */
       exprstat(ls);
      break;
    }
  }
  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); //作用域
}

四、虚拟机篇 - 通过IF语句示例看执行过程


我们接下去通过一个if语句的示例,看Lua代码的解析编译的过程。

//Lua代码
if a > b then
    age = a
else
    age = b
end

//Opcode 
0: LT 0,b,a
1: JMP 4
2: MOV age, a
3: JMP 5
4: MOV age, b

Lua文件解析后,拿到的第一个Token为TK_IF,所以在statement函数中,会调用ifstat

/**
 * 解析语法树,按照块状分割
 */
static void statement (LexState *ls) {
  int line = ls->linenumber;  /* may be needed for error messages */
  enterlevel(ls); //作用域
  switch (ls->t.token) {
    case ';': {  /* stat -> ';' (empty statement) */
      luaX_next(ls);  /* skip ';' */
      break;
    }
    case TK_IF: {  /* stat -> ifstat */
      ifstat(ls, line);
      break;
    }
    ....
}

ifstat函数中,主要通过test_then_block函数,处理IF (cond) THEN block的语法问题。

通过while循环遍历elseif语句,elseif的处理方式也是采用test_then_block方法。

/**
 * 解析if语句:
 * IF (cond) THEN block
 * {ELSEIF (cond) THEN block}
 * [ELSE block]
 * END
 */
static void ifstat (LexState *ls, int line) {
  /* ifstat -> IF cond THEN block {ELSEIF cond THEN block} [ELSE block] END */
  FuncState *fs = ls->fs; //获取函数栈
  int escapelist = NO_JUMP;  /* exit list for finished parts */

  /* 首先解析 if (条件) then 逻辑块 end */
  test_then_block(ls, &escapelist);  /* IF cond THEN block */

  /* 然后解析 elseif (条件) then 逻辑块 end*/
  while (ls->t.token == TK_ELSEIF)
    test_then_block(ls, &escapelist);  /* ELSEIF cond THEN block */

  /* 最后解析 else 逻辑块 end */
  if (testnext(ls, TK_ELSE))
    block(ls);  /* 'else' part */
  check_match(ls, TK_END, TK_IF, line);
  luaK_patchtohere(fs, escapelist);  /* patch escape list to 'if' end */
}

test_then_block函数中,通过luaX_next跳过IF/ELSEIF的token,然后调用expr方法处理条件问题。

条件语句最终会返回true/false的结果值,这个结果值会保存到expdesc结构中。expdesc结构贯穿整个Parse解析编译阶段,主要用于保存每次预发解析的详情信息的传递。

处理完条件语句后,会调用checknext,跳过THEN这个关键字Token,然后继续往下处理。

由于IF语句是一个判断语句,需要根据条件判断的情况,进行执行代码的跳转。这边是通过luaK_goiftrue函数,生成JMP类型的操作码,然后这个操作码最终也会调用luaK_codeABC函数,生成二进制代码,然后放到Proto上。

针对块的内容的解析,会递归回调statlist函数。因为块中的内容,有可能是简答的表达式赋值,也有可能是嵌套的函数、IF语句、FOR循环等。Lua也是有作用域的,所以进入一个块的回调,会执行enterblock函数,离开则执行leaveblock函数。我们看到,我们的IF语句中的块内容是一个表达式,调用statlist后,会进入表达式的处理函数exprstat中。

/**
 * 解析:
 * if (条件) then 逻辑块 end
 * elseif (条件) then 逻辑块 end
 */
static void test_then_block (LexState *ls, int *escapelist) {
  /* test_then_block -> [IF | ELSEIF] cond THEN block */
  BlockCnt bl;
  FuncState *fs = ls->fs;
  expdesc v;
  int jf;  /* instruction to skip 'then' code (if condition is false) */

  luaX_next(ls);  /* skip IF or ELSEIF */
  expr(ls, &v);  /* 条件语句读取,返回结果值存储在v中 read condition */

  checknext(ls, TK_THEN); //下一个Token
  if (ls->t.token == TK_GOTO || ls->t.token == TK_BREAK) {
    luaK_goiffalse(ls->fs, &v);  /* will jump to label if condition is true */
    enterblock(fs, &bl, 0);  /* must enter block before 'goto' */
    gotostat(ls, v.t);  /* handle goto/break */
    while (testnext(ls, ';')) {}  /* skip colons */
    if (block_follow(ls, 0)) {  /* 'goto' is the entire block? */
      leaveblock(fs);
      return;  /* and that is it */
    }
    else  /* must skip over 'then' part if condition is false */
      jf = luaK_jump(fs);
  }
  else {  /* regular case (not goto/break) */
    luaK_goiftrue(ls->fs, &v);  /* skip over block if condition is false */
    enterblock(fs, &bl, 0); //用于管理递归块管理
    jf = v.f;
  }
  statlist(ls);  /* 状态机继续解析IF语句块内语义 'then' part */
  leaveblock(fs);
  if (ls->t.token == TK_ELSE ||
      ls->t.token == TK_ELSEIF)  /* followed by 'else'/'elseif'? */
    luaK_concat(fs, escapelist, luaK_jump(fs));  /* must jump over it */
  luaK_patchtohere(fs, jf);
}

exprstat普通表达式处理逻辑相对比较清晰。主要俩步骤:1. 处理变量名称,可能有多个变量名(LHS_assign) 2. 进行变量赋值,生成Opcode

suffixedexp函数会将变量名信息存储在LHS_assign v结构上。assignment就是真正变量赋值操作,也就是生成Opcode操作。

assignment函数中,主要通过luaK_storevar对变量进行设置值。luaK_storevar函数底层也是调用的luaK_codeABC函数,当然不同类型的变量处理逻辑也是有一些不同的。
Lua的变量逻辑:

  • Lua 中的变量全是全局变量,无论语句块或是函数里,除非用 local 显式声明为局部变量,变量默认值均为nil
  • 使用local创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。
/**
 * 普通表示式处理逻辑
 */
static void exprstat (LexState *ls) {
  /* stat -> func | assignment */
  FuncState *fs = ls->fs;
  struct LHS_assign v; //处理多个值
  suffixedexp(ls, &v.v); //处理变量名

  /* 变量赋值处理 */
  if (ls->t.token == '=' || ls->t.token == ',') { /* stat -> assignment ? */
    v.prev = NULL;
    assignment(ls, &v, 1); //赋值
  }
  else {  /* stat -> func */
    check_condition(ls, v.v.k == VCALL, "syntax error");
    SETARG_C(getinstruction(fs, &v.v), 1);  /* call statement uses no results */
  }
}

/**
 * 变量赋值操作
 * ls:语法解析上下文状态
 * lh:变量名称存储在expdesc结构中,链表形式,可以存储多个变量名
 * nvars:值的个数
 */
static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) {
  expdesc e;
  check_condition(ls, vkisvar(lh->v.k), "syntax error");

  if (testnext(ls, ',')) {  /* assignment -> ',' suffixedexp assignment */
    .........
  }
  else {  /* assignment -> '=' explist */
    int nexps;
    checknext(ls, '='); //跳转到下一个Token
    nexps = explist(ls, &e);
    if (nexps != nvars)
      adjust_assign(ls, nvars, nexps, &e); //调整 判断左边的变量数是否等于右边的值数
    else {
      luaK_setoneret(ls->fs, &e);  /* close last expression */
      luaK_storevar(ls->fs, &lh->v, &e);
      return;  /* avoid default */
    }
  }
  init_exp(&e, VNONRELOC, ls->fs->freereg-1);  /* default assignment */
  luaK_storevar(ls->fs, &lh->v, &e);
}

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐