Lua源码分析 - 虚拟机篇 - 语义解析之Opcode执行(18)
目录虚拟机篇 - 指令执行函数luaV_execute虚拟机篇 - 一个变量赋值操作看实现虚拟机篇 - 指令执行函数luaV_execute在《Lua源码分析 - 主流程篇 - 函数调用栈的实现(08)》我们看到了整个Lua脚本语言的执行主流程。Lua脚本主流程:通过文件解析->解析成语法Token->编译成二进制操作码->执行二进制操作码上一章节我们讲解...
目录
一、虚拟机篇 - 指令执行状态机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状态机之间的关系了。
更多推荐
所有评论(0)