joe 发表于 2022-6-20 16:01:34

Lua源码分析 - 虚拟机篇 - 语义解析之Opcode执行(18)

目录

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

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

一、虚拟机篇 - 指令执行状态机luaV_execute
在《Lua源码分析 - 主流程篇 - 函数调用栈的实现(08)》我们看到了整个Lua脚本语言的执行主流程。

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

上一章节我们讲解了Lua的Opcode的生成。通过luaK_codeAB*函数,实现操作码的封装,二进制操作码会放置在Proto->code数组上。二进制操作码的执行主要是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;
      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;
      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和luaV_execute状态机之间的关系了。
————————————————
版权声明:本文为CSDN博主「老码农zhuli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/initphp/article/details/105411257

页: [1]
查看完整版本: Lua源码分析 - 虚拟机篇 - 语义解析之Opcode执行(18)