【Lua 5.3源码】虚拟机指令分析(三) 赋值指令



1. 赋值指令

Lua中的赋值指令有如下几个:

  • OP_MOVE A B R(A) := R(B)
  • OP_LOADK A Bx R(A) := Kst(Bx)
  • OP_LOADKX A R(A) := Kst(extra arg)
  • OP_LOADBOOL A B C R(A) := (Bool)B; if (C) pc++
  • OP_LOADNIL A B R(A), R(A+1), …, R(A+B) := nil
  • OP_GETUPVAL A B R(A) := UpValue[B]
  • OP_GETTABUP A B C R(A) := UpValue[B][RK(C)]
  • OP_GETTABLE A B C R(A) := R(B)[RK(C)]
  • OP_SETTABUP A B C UpValue[A][RK(B)] := RK(C)
  • OP_SETUPVAL A B UpValue[B] := R(A)
  • OP_SETTABLE A B C R(A)[RK(B)] := RK(C)

下面会选择其中几个指令进行分析:



2. OP_MOVE:

vmcase(OP_MOVE) {
  setobjs2s(L, ra, RB(i));
  vmbreak;
}

首先看RB、check_exp、GET_OPCODE、GETARG_B宏

#define RB(i)	check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i))
#define check_exp(c,e)		(lua_assert(c), (e))
#define getBMode(m)	(cast(enum OpArgMask, (luaP_opmodes[m] >> 4) & 3))
#define GETARG_B(i)	getarg(i, POS_B, SIZE_B)

可见check_exp宏其实是用来断言检测的,getBMode是用来获取指令的B参数格式,配合check_exp来进行判断指令格式是否合法。
最重要的是base+GETARG_B(i):

  • base记录的是栈基址,也就是函数栈的起始位置,在这里表示寄存器的起始地址。
  • GETARG_B(i),对于OP_MOVE指令来说,B参数表示一个寄存器的索引,而GETARG_B(i)就是获取这个索引值
  • base+GETARG_B(i),获取到B参数对应寄存器的值。

然后再看setobjs2s相关的代码

#define setobjs2s	setobj
#define setobj(L,obj1,obj2) \
	{ TValue *io1=(obj1); *io1 = *(obj2); \
	  (void)L; checkliveness(L,io1); }

可以看出setobjs2s(L, ra, RB(i)),就是将RB(i)寄存器的值赋值给ra,那么ra又是什么呢?其实在vmfetch中进行了初始化:

/* fetch an instruction and prepare its execution */
#define vmfetch()	{ \
  i = *(ci->u.l.savedpc++); \
  if (L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) \
    Protect(luaG_traceexec(L)); \
  ra = RA(i); /* WARNING: any stack reallocation invalidates 'ra' */ \
  lua_assert(base == ci->u.l.base); \
  lua_assert(base <= L->top && L->top < L->stack + L->stacksize); \
}

可以看到,ra就是RA(i)寄存器中的值(和RB同理)。

那么OP_MOVE指令就是将B寄存器的值赋值给A寄存器。我们可以看下如下代码编译后的字节码:

local a = 1
local b = a
main  (3 instructions at 0083A2B0)
0+ params, 2 slots, 1 upvalue, 2 locals, 1 constant, 0 functions
	1	[1]	LOADK    	0 -1	; 1
	2	[2]	MOVE     	1 0
	3	[2]	RETURN   	0 1
constants (1) for 0083A2B0:
	1	1
locals (2) for 0083A2B0:
	0	a	2	4
	1	b	3	4
upvalues (1) for 0083A2B0:
	0	_ENV	1	0

可以看到,局部变量a存放在0号寄存器上,b存放在1号寄存器上,PC[1]指令表示将0号寄存器的值赋值给1号寄存器;也就是 b = a。



3. OP_LOADK:

vmcase(OP_LOADK) {
  TValue *rb = k + GETARG_Bx(i);
  setobj2s(L, ra, rb);
  vmbreak;
}

在分析过OP_MOVE之后,对于GETARG_Bx,setobj2s已经不陌生了。
但是这里需要关注下k

k = cl->p->k;  /* local reference to function's constant table */

k表示的是常量表的地址,而OP_LOADK的BX参数就是表示常量表的索引;所以k + GETARG_Bx(i)表示的是常量上Bx索引对应的值。

那么OP_LOADK指令就是将BX索引对应的常量赋值给A寄存器。我们可以看下如下代码编译后的字节码:

local a = 1
main  (2 instructions at 00A3A2B0)
0+ params, 2 slots, 1 upvalue, 1 local, 1 constant, 0 functions
	1	[1]	LOADK    	0 -1	; 1
	2	[1]	RETURN   	0 1
constants (1) for 00A3A2B0:
	1	1
locals (1) for 00A3A2B0:
	0	a	2	3
upvalues (1) for 00A3A2B0:
	0	_ENV	1	0

可以看到,在编译过程中,Lua解析到常量1,放置在常量表1号位置。局部变量a存放在0号寄存器上,而LOADK 0 -1 表示将常量表1号位置的值赋值给0号寄存器。(注意为什么这里是-1,是因为Lua虚拟机通过最高位为0,1来区分是寄存器上的索引还是常量表上的索引)。

然而由于Bx 有 18 bit 长,所以 LOADK 这个操作只能索引到 2^17 个常量。 为了扩大索引常量的上限,提供了LOADKX,它将常量索引号放在了接下来的一条EXTRAARG 指令中。 OP_EXTRAARG 指令 把 opcode所占的 8bit 以外的26 bit 都用于参数表示, 称之为* Ax*。



4. OP_LOADNIL:

vmcase(OP_LOADNIL) {
  int b = GETARG_B(i);
  do {
    setnilvalue(ra++);
  } while (b--);
  vmbreak;
}

从上面代码可以看出OP_LOADNIL是可以同时对一段连续的寄存器都赋予nil值,而个数则为指令的B参数。
所以OP_LOADNIL指令将从A号寄存器开始的连续B个寄存器值都赋予为nil。

所以,如果是对于连续的寄存器的赋值nil,lua编译器会合并到一个指令,而不连续的就不能合并,我们在编写的时候可以注意这个细节:

分开写:

local a;
local b = 1
local c;    --这里不能合并,需要三个指令
main  (4 instructions at 004BA2B0)
0+ params, 3 slots, 1 upvalue, 3 locals, 1 constant, 0 functions
	1	[1]	LOADNIL  	0 0
	2	[2]	LOADK    	1 -1	; 1
	3	[3]	LOADNIL  	2 0
	4	[3]	RETURN   	0 1
constants (1) for 004BA2B0:
	1	1
locals (3) for 004BA2B0:
	0	a	2	5
	1	b	3	5
	2	c	4	5
upvalues (1) for 004BA2B0:
	0	_ENV	1	0

合并写:

local a;
local c;       --或者可以 local a,c 
local b = 1  --这里可以合并,需要两个指令
main  (3 instructions at 003DA2B0)
0+ params, 3 slots, 1 upvalue, 3 locals, 1 constant, 0 functions
	1	[1]	LOADNIL  	0 1
	2	[3]	LOADK    	2 -1	; 1
	3	[3]	RETURN   	0 1
constants (1) for 003DA2B0:
	1	1
locals (3) for 003DA2B0:
	0	a	2	4
	1	c	2	4
	2	b	3	4
upvalues (1) for 003DA2B0:
	0	_ENV	1	0



5. OP_LOADBOOL:

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

从代码就可以看出来OP_LOADBOOL是将B参数作为bool值赋值给A寄存器,然后取C参数,判断是否为真,如果为真则跳过下一个指令。前面两个参数A,B很容易理解,但是为什么LOADBOOL指令会设计一个C参数进行指令跳转呢?

原因是因为,Lua虚拟机在设计之初对关系逻辑判断(>=,<= , == , ~=)导致的,Lua虚拟机对待有条件判断的语句的处理都是将所有分支的指令都生成出来然后通过OP_LT,OP_JMP指令来进行跳转,执行真正的分支。这里我们暂时不对OP_LT,OP_JMP细讲,只是简单描述用于理解OP_LOADBOOL的C参数的实际意义:

local a = false
local b = 3 > a
local c = 1 < 3
main  (10 instructions at 0083A2B0)
0+ params, 3 slots, 1 upvalue, 3 locals, 2 constants, 0 functions
	1	[1]	LOADBOOL 	0 0 0
	2	[2]	LT       	1 0 -1	; - 3
	3	[2]	JMP      	0 1	; to 5
	4	[2]	LOADBOOL 	1 0 1
	5	[2]	LOADBOOL 	1 1 0
	6	[3]	LT       	1 -2 -1	; 1 3
	7	[3]	JMP      	0 1	; to 9
	8	[3]	LOADBOOL 	2 0 1
	9	[3]	LOADBOOL 	2 1 0
	10	[3]	RETURN   	0 1
constants (2) for 0083A2B0:
	1	3
	2	1
locals (3) for 0083A2B0:
	0	a	2	11
	1	b	6	11
	2	c	10	11
upvalues (1) for 0083A2B0:
	0	_ENV	1	0

通过指令前面的行号,我们可以看出

  • 第一个语句生成了PC[1]指令
  • 第二个语句生成了PC[2]~PC[5]
  • 第三个语句生成了PC[6]~PC[9] (这里忽略PC[10],因为lua将文件当做闭包来处理,所以在最后一个语句会自动加上一个return指令)

首先第一个语句,很容易理解 a = false,其实在编译阶段就将结果直接存储在指令中作为立即数;翻译一下指令就是:将0(false)作为bool变量赋值给0号寄存器(a)。

第二个语句,涉及到一个关系表达式的求值,表达式子的结果赋值给b;如果是传统的栈式虚拟机,可能就是先将表达式求值结果入栈,然后再出栈赋值,是一个线性的顺序没有分支。但是在Lua虚拟机中的流程是如下:

if 3 > a then
	b = true
else
    b = false
end

Lua编译器会生成两个OP_LOADBOOL的指令分别是赋值false,true。
然后通过前面两个指令OP_LT,OP_JMP控制虚拟机跳转到对应的OP_LOADBOOL指令。

但是这里有个问题,因为指令的执行是连续的,假如前面的OP_LT和OP_JMP指令跳转到第一个OP_LOADBOOL指令,那岂不是第二个OP_LOADBOOL也会被执行?

所以OP_LOADBOOL设计了C参数,如果C为true,则跳过下一个指令;所以仔细看第4个指令

LOADBOOL 	1 0 1

它的C参数为1,目的就是用来跳过下面一个OP_LOADBOOL指令。



6. OP_GETTABLE:

vmcase(OP_GETTABLE) {
  StkId rb = RB(i);
  TValue *rc = RKC(i);
  gettableProtected(L, rb, rc, ra);
  vmbreak;
}

这里RB宏不多叙述,表示取指令B参数对应的寄存器的值。
对于RKC宏:

#define RKC(i)	check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \
	ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))

从名字可以看出,获取指令C参数,判断是常量索引还是寄存器索引,然后取对应的值。

对于gettableProtected方法,我们看源码:

#define gettableProtected(L,t,k,v)  { const TValue *slot; \
  if (luaV_fastget(L,t,k,slot,luaH_get)) { setobj2s(L, v, slot); } \
  else Protect(luaV_finishget(L,t,k,v,slot)); }

#define luaV_fastget(L,t,k,slot,f) \
  (!ttistable(t)  \
   ? (slot = NULL, 0)  /* not a table; 'slot' is NULL and result is 0 */  \
   : (slot = f(hvalue(t), k),  /* else, do raw access */  \
      !ttisnil(slot)))  /* result not nil? */

对于luaH_get方法,之前在table那一篇文章讲过,是从一个table表里获取对应Key的Value。

luaV_fastget函数的意思就是:

  1. 首先判断t是否是一个table,如果不是就将slot置为NULL,并返回false,
  2. 调用luaH_get获取table中key对应的值,并复制给slot,如果不为nil返回true,否则返回false。

所以gettableProtected(L,t,k,v)就是先尝试从t中获取k对应的value,如果获取到了就赋值给v,如果没有找到就调用Protect(luaV_finishget(L,t,k,v,slot))

继续阅读Project宏和luaV_finishget方法:

#define Protect(x)	{ {x;}; base = ci->u.l.base; }

/*
** Finish the table access 'val = t[key]'.
** if 'slot' is NULL, 't' is not a table; otherwise, 'slot' points to
** t[k] entry (which must be nil).
*/
void luaV_finishget (lua_State *L, const TValue *t, TValue *key, StkId val,
                      const TValue *slot) {
  int loop;  /* counter to avoid infinite loops */
  const TValue *tm;  /* metamethod */
  for (loop = 0; loop < MAXTAGLOOP; loop++) {
    if (slot == NULL) {  /* 't' is not a table? */
      lua_assert(!ttistable(t));
      tm = luaT_gettmbyobj(L, t, TM_INDEX);
      if (ttisnil(tm))
        luaG_typeerror(L, t, "index");  /* no metamethod */
      /* else will try the metamethod */
    }
    else {  /* 't' is a table */
      lua_assert(ttisnil(slot));
      tm = fasttm(L, hvalue(t)->metatable, TM_INDEX);  /* table's metamethod */
      if (tm == NULL) {  /* no metamethod? */
        setnilvalue(val);  /* result is nil */
        return;
      }
      /* else will try the metamethod */
    }
    if (ttisfunction(tm)) {  /* is metamethod a function? */
      luaT_callTM(L, tm, t, key, val, 1);  /* call it */
      return;
    }
    t = tm;  /* else try to access 'tm[key]' */
    if (luaV_fastget(L,t,key,slot,luaH_get)) {  /* fast track? */
      setobj2s(L, val, slot);  /* done */
      return;
    }
    /* else repeat (tail call 'luaV_finishget') */
  }
  luaG_runerror(L, "'__index' chain too long; possible loop");
}

可以从代码看出luaV_finishget就是尝试获取table的元表或者原方法,然后循环去获取Key对应的值,不过为了避免元表循环引用或者死循环,Lua设置了最大循环次数MAXTAGLOOP。

而Protect(x)宏,是为了避免调用x里面的代码修改了栈基址,所以再次对base进行了修正。

综上,可以看出gettableProtected(L,t,k,v)宏就是从t里面获取key对应的值并赋值给v。



7. OP_GETUPVAL:

vmcase(OP_GETUPVAL) {
  int b = GETARG_B(i);
  setobj2s(L, ra, cl->upvals[b]->v);
  vmbreak;
}

看代码很容易理解,OP_GETUPVAL的B参数为Upvalue的索引,获取B参数对应的的Upvalue值,然后设置到A寄存器上。

local val = 1
function test()
	local pos = val
end
main  (4 instructions at 0014A2B0)
0+ params, 2 slots, 1 upvalue, 1 local, 2 constants, 1 function
	1	[1]	LOADK    	0 -1	; 1
	2	[4]	CLOSURE  	1 0	; 0014B5F8
	3	[2]	SETTABUP 	0 -2 1	; _ENV "test"
	4	[4]	RETURN   	0 1
constants (2) for 0014A2B0:
	1	1
	2	"test"
locals (1) for 0014A2B0:
	0	val	2	5
upvalues (1) for 0014A2B0:
	0	_ENV	1	0

function  (2 instructions at 0014B5F8)
0 params, 2 slots, 1 upvalue, 1 local, 0 constants, 0 functions
	1	[3]	GETUPVAL 	0 0	; val
	2	[4]	RETURN   	0 1
constants (0) for 0014B5F8:
locals (1) for 0014B5F8:
	0	pos	2	3
upvalues (1) for 0014B5F8:
	0	val	1	0

这段代码有一个函数会生成两个闭包(文件本身会生成一个闭包),在函数test中,用val变量对pos变量赋值;Lua编译器在语义分析val变量的时候,发现val变量不存在当前函数的局部变量中,就尝试去函数的Upvalue列表中找,发现test闭包中的确存在val这个Upvalue。所以生成GETUPVAL 0 0指令,将第0个Upvalue的值赋值给第0个寄存器。

那么问题来了,如果编译器在函数的Upvalue列表中没有找到这个变量呢?那么这个变量会被认定为是_G 中的值(全局变量)。如下:



8. OP_GETTABUP:

vmcase(OP_GETTABUP) {
  TValue *upval = cl->upvals[GETARG_B(i)]->v;
  TValue *rc = RKC(i);
  gettableProtected(L, upval, rc, ra);
  vmbreak;
}

这个指令很容易理解,从Upvalue列表中,获取指令B参数对应的Upvalue,并用指令C参数作为Key从Upvalue里面获取对应的值赋值给A寄存器。

样例代码和上面那个很类似,只不过少了一句val变量的定义

function test()
	local pos = val
end
main  (3 instructions at 0068A2B0)
0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function
	1	[3]	CLOSURE  	0 0	; 0068B428
	2	[1]	SETTABUP 	0 -1 0	; _ENV "test"
	3	[3]	RETURN   	0 1
constants (1) for 0068A2B0:
	1	"test"
locals (0) for 0068A2B0:
upvalues (1) for 0068A2B0:
	0	_ENV	1	0

function  (2 instructions at 0068B428)
0 params, 2 slots, 1 upvalue, 1 local, 1 constant, 0 functions
	1	[2]	GETTABUP 	0 0 -1	; _ENV "val"
	2	[3]	RETURN   	0 1
constants (1) for 0068B428:
	1	"val"
locals (1) for 0068B428:
	0	pos	2	3
upvalues (1) for 0068B428:
	0	_ENV	0	0

可以看出,在这个代码中,没有定义val变量;所以Lua编译器在语义分析val变量的时候,并没有从test方法的Upvalue列表里面找到val变量。所以Lua编译器认定val是一个全局变量,在_G表中。所以test方法的Upvalue列表里面有_ENV(就是_G表)。而生成的指令则为OP_GETTABUP,意思是从upvalue中的table表里获取值,这里的Upvalue是_G表,是一个table;这里的key就是"val"。

翻译一下:GETTABUP 0 0 -1 ; _ENV “val”

R[0] = Upvalue[0][K[1]]

  • R[0] 是第0个寄存器
  • Upvalue[0] 是_G表
  • K[1] 是“val”

9. 其他:

  • OP_SETTABUP A B C UpValue[A][RK(B)] := RK(C)
  • OP_SETUPVAL A B UpValue[B] := R(A)
  • OP_SETTABLE A B C R(A)[RK(B)] := RK(C)

剩下这三个指令是对应的SET指令,和GET指令类似,所以这里不再累述。

你可能感兴趣的:(Lua学习分享)