目录

一、虚拟机篇 - 指令执行状态机luaV_execute

二、虚拟机篇 - 状态机的具体实现原理


一、虚拟机篇 - 指令执行状态机luaV_execute


《Lua源码分析 - 主流程篇 - 函数调用栈的实现(08)》我们看到了整个Lua脚本语言的执行主流程。

Lua脚本执行流程:文件读取->解析成语法Token->编译成二进制操作码->执行二进制操作码

上一章节我们讲解了Lua的Opcode的生成。通过luaK_codeAB*函数,实现操作码的封装,二进制操作码会放置在Proto->code[n]数组上。二进制操作码的执行主要是lvm.c文件中的luaV_execute,通过循环遍历操作码数组来实现程序的指令的执行。

luaV_execute函数有几个要点:

  • luaV_execute函数是一个循环遍历的状态机,通过遍历二进制操作码的数组,逐个执行指令
  • 不同的Opcode的类型,执行的数据操作和栈操作都不一样,所以通过switch case的进行选择操作
  • 我们看到遍历的是ci->u.l.savedpc,而并非Proto->code数组。其实在luaD_precall函数中,进行了赋值操作ci->u.l.savedpc = p->code
  • OP_MOVE操作是一个对象变量之间的赋值操作。参数ra为操作码中的A参数,通过RA函数获取;参数rb为操作码中的B参数,通过RB函数获取。通过setobjs2s函数,将rb对象设置到ra上。
//获取二进制操作码的Opcode值和参数A、B、C
#define RA(i)	(base+GETARG_A(i))
#define RB(i)	check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i))
#define RC(i)	check_exp(getCMode(GET_OPCODE(i)) == OpArgR, base+GETARG_C(i))
#define RKB(i)	check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \
	ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
#define RKC(i)	check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \
	ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))

//操作码执行函数
void luaV_execute (lua_State *L) {
  CallInfo *ci = L->ci;
  LClosure *cl;
  TValue *k;
  StkId base;
  ci->callstatus |= CIST_FRESH;  /* fresh invocation of 'luaV_execute" */
 newframe:  /* reentry point when frame changes (call/return) */
  lua_assert(ci == L->ci);
  cl = clLvalue(ci->func);  /* local reference to function's closure */
  k = cl->p->k;  /* local reference to function's constant table */
  base = ci->u.l.base;  /* local copy of function's base */
  /* main loop of interpreter */
  for (;;) {
    Instruction i;
    StkId ra;
    vmfetch();
    vmdispatch (GET_OPCODE(i)) {
    	//变量赋值操作
      vmcase(OP_MOVE) {
        setobjs2s(L, ra, RB(i));
        vmbreak;
      }
      //加载布尔值
      vmcase(OP_LOADBOOL) {
        setbvalue(ra, GETARG_B(i));
        if (GETARG_C(i)) ci->u.l.savedpc++;  /* skip next instruction (if C) */
        vmbreak;
      }
      vmcase(OP_LOADNIL) {
        int b = GETARG_B(i);
        do {
          setnilvalue(ra++);
        } while (b--);
        vmbreak;
      }
.....
      //全局变量设置操作
      vmcase(OP_SETUPVAL) {
        UpVal *uv = cl->upvals[GETARG_B(i)];
        setobj(L, uv->v, ra);
        luaC_upvalbarrier(L, uv);
        vmbreak;
      }
......
      }
    }
  }
}

二、虚拟机篇 - 状态机的具体实现原理


我们通过一个赋值的案例来看整个Lua的Opcode的执行过程。

先看一个lua例子,这个案例中有两点需要注意:

  • age是一个全局变量,针对全局生效
  • age2 是一个local状态的局部变量,仅对当前代码块生效
age=5;
local age2 = age;

上面的案例是普通的表达式赋值。上一章节,我们说过,Opcode的生成是通过statlist中的语法块解析状态机实现的。普通的赋值表达式,就会进入默认的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_LOCAL: {  /* stat -> localstat */
      luaX_next(ls);  /* skip LOCAL */
      if (testnext(ls, TK_FUNCTION))  /* local function? */
        localfunc(ls);
      else
        localstat(ls);
      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); //作用域
}

如果是local局部变量,先会进行TK_LOCAL分支的逻辑处理,通过new_localvar生成局部变量名称的数组,在Proto->locvars[]数组上进行管理。local的token处理完之后,又会进入正常赋值表达式逻辑进行处理,所以又会进入分支:exprstat。

static void localstat (LexState *ls) {
  /* stat -> LOCAL NAME {',' NAME} ['=' explist] */
  int nvars = 0;
  int nexps;
  expdesc e;
  do {
    new_localvar(ls, str_checkname(ls)); //管理本地变量名
....
}

在exprstat表达式处理流程中,主要两个操作:变量处理、赋值操作

  • suffixedexp函数:主要用来处理赋值变量名称,判断变量的类型:局部变量、全局变量、Table格式、函数等。
  • assignment函数:主要用于变量的赋值操作。例如局部变量、全局变量等通过luaK_codeAB*函数,生成32位的二进制操作码
/**
 * 普通表示式处理逻辑
 */
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 */
  }
}

如果我们的变量名称是普通的变量名,则跟踪suffixedexp函数,最终会进入singlevar函数。

  • 通过str_checkname函数,获取变量名称的字符串内存地址
  • 通过singlevaraux处理局部变量local和全局变量upvalue
  • 局部变量local:局部变量我们上面说过,通过状态机里面TK_LOCAL分支逻辑,将变量name存放到Proto->locvars[]数组上。通过searchvar函数,查询局部变量的数组表,找到对应的数组下标
  • 全局变量upvalue:Lua语言中,只要没有local标识的变量都为全局变量,不受代码块的限制和影响。如果变量是全局变量,则通过searchupvalue函数查询全局变量名称,如果没有找到则通过newupvalue函数,到Proto->upvalues[]数组上,创建一个值
//单个变量名称处理
// * Lua 中的变量全是全局变量,无论语句块或是函数里,除非用 local 显式声明为局部变量,变量默认值均为nil
// 使用local创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。
static void singlevar (LexState *ls, expdesc *var) {
  TString *varname = str_checkname(ls); //获取变量名称  获取的时候执行了:luaX_next
  FuncState *fs = ls->fs;
  singlevaraux(fs, varname, var, 1); //判断变量类型 是local、upvalue
  if (var->k == VVOID) {  /* global name? */
.....
  }
}

/*
  Find variable with given name 'n'. If it is an upvalue, add this
  upvalue into all intermediate functions.
*/
static void singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
  if (fs == NULL)  /* 全局变量 no more levels? */
    init_exp(var, VVOID, 0);  /* 全局变量 default is global */
  else {
    int v = searchvar(fs, n);  /* 从函数局部变量中查找局部变量值 look up locals at current level */
    if (v >= 0) {  /* found? */
      init_exp(var, VLOCAL, v);  /* 局部变量 variable is local */
      if (!base)
        markupval(fs, v);  /* local will be used as an upval */
    }
    else {  /* not found as local at current level; try upvalues */
    	//查询全局变量,如果没有找到全局变量,则newupvalue重新生成
      int idx = searchupvalue(fs, n);  /* try existing upvalues */
      if (idx < 0) {  /* not found? */
        singlevaraux(fs->prev, n, var, 0);  /* try upper levels */
        if (var->k == VVOID)  /* not found? */
          return;  /* it is a global */
        /* else was LOCAL or UPVAL */
        idx  = newupvalue(fs, n, var);  /* will be a new upvalue */
      }
      init_exp(var, VUPVAL, idx);  /* new or old upvalue */
    }
  }
}

我们继续回到exprstat函数中,看一下赋值操作assignment函数

  • 如果变量有多个值赋值,则会递归调用assignment函数,直到多个值都赋值完毕
  • 正常情况下,就会进入=号的赋值操作,主要调用luaK_storevar函数,实现各种不同变量的赋值操作码生成工作。
/**
 * 变量赋值操作
 * 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 */
    struct LHS_assign nv;
    nv.prev = lh;
    suffixedexp(ls, &nv.v);
    if (nv.v.k != VINDEXED)
      check_conflict(ls, lh, &nv.v);
    checklimit(ls->fs, nvars + ls->L->nCcalls, LUAI_MAXCCALLS,
                    "C levels");
    assignment(ls, &nv, nvars+1);
  }
  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);
}

luaK_storevar函数中,通过变量的类型,来区分不同的操作。主要分为:局部变量、全局变量、下标类型

  • 局部变量:主要调用exp2reg函数,该函数底层调用discharge2reg函数,通过值的不同类型,来实现不同的操作码生成操作
  • 全局变量:全局变量首先会调用luaK_exp2anyreg函数,实际底层也是调用了exp2reg函数,针对不同值类型进行不同的操作码封装操作。然后调用luaK_codeABC函数,进行OP_SETUPVAL全局变量的设置操作。
  • 下标类型:通过变量类型,来确定OP_SETTABLE或者OP_SETTABUP操作符,并调用luaK_codeABC函数进行操作码封装。
void luaK_storevar (FuncState *fs, expdesc *var, expdesc *ex) {
  switch (var->k) {
  	  //局部变量,需要声明 local 标识
    case VLOCAL: {
      freeexp(fs, ex);
      //A=结果 B=变量
      exp2reg(fs, ex, var->u.info);  /* compute 'ex' into proper place */
      return;
    }
    // Lua除了局部变量外,都是全局变量
    case VUPVAL: {
      int e = luaK_exp2anyreg(fs, ex); //底下也是调用exp2reg函数,主要用于将值设置到变量上
      luaK_codeABC(fs, OP_SETUPVAL, e, var->u.info, 0); //全局变量设置一下
      break;
    }
    //Table格式
    case VINDEXED: {
      OpCode op = (var->u.ind.vt == VLOCAL) ? OP_SETTABLE : OP_SETTABUP;
      int e = luaK_exp2RK(fs, ex);
      luaK_codeABC(fs, op, var->u.ind.t, var->u.ind.idx, e);
      break;
    }
    default: lua_assert(0);  /* invalid var kind to store */
  }
  freeexp(fs, ex);
}

discharge2reg函数是底层赋值的操作函数。针对值的不同类型进行不同的封装操作码。

  • 布尔类型:则通过luaK_codeABC函数,封装OP_LOADBOOL操作符,参数A为变量名称,参数B为布尔值
  • 对象赋值:如果是两个对象变量之间的赋值,则会封装OP_MOVE操作符,参数A为变量名称,参数B为赋值变量对象地址
  • 全局变量操作:全局变量OP_SETUPVAL操作符,参数A为值,B为变量名称值(这里不太一样)
      	//变量赋值操作
      vmcase(OP_MOVE) {
        setobjs2s(L, ra, RB(i));
        vmbreak;
      }

      //加载布尔值
      vmcase(OP_LOADBOOL) {
        setbvalue(ra, GETARG_B(i));
        if (GETARG_C(i)) ci->u.l.savedpc++;  /* skip next instruction (if C) */
        vmbreak;
      }

     //全局变量设置操作
      vmcase(OP_SETUPVAL) {
        UpVal *uv = cl->upvals[GETARG_B(i)];
        setobj(L, uv->v, ra);
        luaC_upvalbarrier(L, uv);
        vmbreak;
      }

static void discharge2reg (FuncState *fs, expdesc *e, int reg) {
  luaK_dischargevars(fs, e);
  switch (e->k) {
    case VNIL: {
      luaK_nil(fs, reg, 1);
      break;
    }
    case VFALSE: case VTRUE: {
      luaK_codeABC(fs, OP_LOADBOOL, reg, e->k == VTRUE, 0);
      break;
    }
    case VK: {
      luaK_codek(fs, reg, e->u.info);
      break;
    }
    case VKFLT: {
      luaK_codek(fs, reg, luaK_numberK(fs, e->u.nval));
      break;
    }
    case VKINT: {
      luaK_codek(fs, reg, luaK_intK(fs, e->u.ival));
      break;
    }
    //全局变量处理
    case VRELOCABLE: {
      Instruction *pc = &getinstruction(fs, e);
      SETARG_A(*pc, reg);  /* instruction will put result in 'reg' */
      break;
    }
    case VNONRELOC: {
      //A是变量 B是值  变量之间赋值
      if (reg != e->u.info)
        luaK_codeABC(fs, OP_MOVE, reg, e->u.info, 0);
      break;
    }
    default: {
      lua_assert(e->k == VJMP);
      return;  /* nothing to do... */
    }
  }
  e->u.info = reg;
  e->k = VNONRELOC;
}

看完上面的case,基本大家就能明白,二进制操作码数组Proto->code[n]和luaV_execute状态机之间的关系了。

Logo

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

更多推荐