目录

一、虚拟机篇 - 指令执行状态机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);elselocalstat(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_nextFuncState *fs = ls->fs;singlevaraux(fs, varname, var, 1); //判断变量类型 是local、upvalueif (var->k == VVOID) {  /* global name? */
.....}
}/*Find variable with given name 'n'. If it is an upvalue, add thisupvalue 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, '='); //跳转到下一个Tokennexps = 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状态机之间的关系了。

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

  1. Lua源码分析 - 虚拟机篇 - 语义解析之Opcode生成(17)

    目录 一.虚拟机篇 - 指令集存储数据结构Proto 二.虚拟机篇 - Opcode的定义和宏函数 三.虚拟机篇 - 核心函数luaK_codeABC和luaK_codeABx 四.虚拟机篇 - 常量 ...

  2. Lua源码分析 - 基础篇 - Lua源码的结构和架构图(01)

    目录 一.Lua语言简介 二.Lua架构图 三.Lua源码结构 很久很久没有写博客了,一直忙于工作和项目,最近依然想静下来阅读一些好的源码.自从读完了Nginx和Memcache的源码后,对服务器端的 ...

  3. Spring 源码分析衍生篇十 :Last-Modified 缓存机制

    文章目录 一.前言 二.Last-Modify 三.实现方案 1. 实现 org.springframework.web.servlet.mvc.LastModified接口 1.1. 简单演示 1. ...

  4. asp.net mvc源码分析-Action篇 Action的执行

    接着上篇 asp.net mvc源码分析-Action篇 DefaultModelBinder 我们已经获取的了Action的参数,有前面的内容我们知道Action的调用时在ControllerAct ...

  5. asp.net mvc源码分析-Action篇 DefaultModelBinder

    接着上篇 asp.net mvc源码分析-Controller篇 ValueProvider 现在我们来看看ModelBindingContext这个对象. ModelBindingContext b ...

  6. asp.net mvc源码分析-Controllerl篇 ControllerDescriptor

    在上篇asp.net mvc源码分析-Controllerl篇 TempData数据存储 我们讲到了ActionInvoker.InvokeAction(ControllerContext, acti ...

  7. 鸿蒙系统源代码解析,鸿蒙内核源码分析(系统调用篇) | 图解系统调用全貌

    本篇说清楚系统调用 读本篇之前建议先读鸿蒙内核源码分析(总目录)工作模式篇. 本篇通过一张图和七段代码详细说明系统调用的整个过程,代码一捅到底,直到汇编层再也捅不下去. 先看图,这里的模式可以理解为空 ...

  8. 鸿蒙内核代码 行,鸿蒙内核源码分析(CPU篇) | 内核是如何描述CPU的 ? | 祝新的一年牛气冲天 ! | v36.01...

    本篇说清楚CPU 读本篇之前建议先读鸿蒙内核源码分析(总目录)进程/线程篇.指令是稳定的,但指令序列是变化的,只有这样计算机才能够实现用计算来解决一切问题这个目标.计算是稳定的,但计算的数据是多变的, ...

  9. 鸿蒙内核 cpu兼容,鸿蒙内核源码分析(CPU篇) | 整个内核就是一个死循环 | 祝新的一年牛气冲天 ! | v32.04...

    本篇说清楚CPU 读本篇之前建议先读鸿蒙内核源码分析(总目录)进程/线程篇. 指令是稳定的,但指令序列是变化的,只有这样计算机才能够实现用计算来解决一切问题这个目标.计算是稳定的,但计算的数据是多变的 ...

最新文章

  1. 详细记录python的range()函数用法
  2. 有关c++中const用法
  3. 微信小程序学习笔记(五)
  4. Flex网站作品“妙句网”简化版推出(服务端为.Net WebService)
  5. 《Linux命令行与shell脚本编程大全 第3版》Linux命令行---4
  6. vs2010编写的net3.5用vs2008打开
  7. java work set没有显示出来_eclipse里新建work set,将项目分组放在不同文件夹
  8. Qt 翻译文件的加载
  9. 【debug】UnboundLocalError local variable a referenced before assignment
  10. JavaScript基础知识(三个判断、三个循环)
  11. 关于java代码中的注释问题。(类中方法的注释,我们一般都要写上这个方法的文档(doc),方法的参数也要有它的文档)
  12. opencv图像连通区域分析
  13. 人大金仓数据库 Windows安装教程 -kingbase8R6
  14. 从“断臂求生”到一骑绝尘,航运巨头马士基如何利用区块链技术力挽狂澜?
  15. c# Stack源码解析
  16. 华为笔记本没有网线口_matebook 14有网线接口吗
  17. 力扣 347. 前 K 个高频元素
  18. AGV机器人出圈:助力产线物流自动化
  19. linux替换文件中内容
  20. tplink 2.4g弱信号剔除_如何区分2.4G和5G路由器之间的区别?2.4G和5G路由器那个更好?...

热门文章

  1. SIMCOM模块介绍
  2. ElasticSearch7.X和ElasticSearch8.X学习记录
  3. 一些keil编程错误总结
  4. SpringBoot: Redis 模拟高并发商品秒杀测试
  5. 74HC595 8位移位寄存器介绍
  6. 友善之臂 mini2440 linux led 驱动代码,友善之臂mini2440的LEDdriver驱动分析及测试程序...
  7. 和Sun一起开源(武林外传游戏外挂Java源代码) - 论坛版
  8. iatf16949内审员_IATF16949:2016汽车质量管理体系内审员
  9. word中给论文标参考文献和文献序号变化后的自动更新
  10. 【3D目标检测】KITTI数据集