Stack based vs Register based VM
可直接参考 Stack based vs Register based VM
lua函数调用
先看一下lua函数的结构:
/*
** Function Prototypes
*/
typedef struct Proto {
CommonHeader;
TValue *k; /* constants used by the function */
Instruction *code;
struct Proto **p; /* functions defined inside the function */
int *lineinfo; /* map from opcodes to source lines */
struct LocVar *locvars; /* information about local variables */
TString **upvalues; /* upvalue names */
TString *source;
int sizeupvalues;
int sizek; /* size of `k' */
int sizecode;
int sizelineinfo;
int sizep; /* size of `p' */
int sizelocvars;
int linedefined;
int lastlinedefined;
GCObject *gclist;
lu_byte nups; /* number of upvalues */
lu_byte numparams;
lu_byte is_vararg;
lu_byte maxstacksize;
} Proto;
/*
** Upvalues
*/
typedef struct UpVal {
CommonHeader;
TValue *v; /* points to stack or to its own value */
union {
TValue value; /* the value (when closed) */
struct { /* double linked list (when open) */
struct UpVal *prev;
struct UpVal *next;
} l;
} u;
} UpVal;
#define ClosureHeader \
CommonHeader; lu_byte isC; lu_byte nupvalues; GCObject *gclist; \
struct Table *env
typedef struct {
ClosureHeader;
Proto *p;
UpVal *upvals[1];
} LClosure;
对于lua函数,luaD_precall已处理好一些前置操作,比如参数处理、增加调用栈等,然后调用luaV_execute去执行lua函数的每条指令。
特别的看一下数据栈的处理,在编译时已确定每个lua函数执行过程中数据栈的最大大小,将ci->top/L->top直接设为最大值,[L->base, L->top)就做为lua指令的“寄存器空间”使用,访问寄存器就是以下标访问base数组。
int luaD_precall (lua_State *L, StkId func, int nresults) {
...
ci->top = L->base + p->maxstacksize;
lua_assert(ci->top <= L->stack_last);
L->savedpc = p->code; /* starting point */
ci->tailcalls = 0;
ci->nresults = nresults;
for (st = L->top; st < ci->top; st++)
setnilvalue(st);
L->top = ci->top;
...
指令格式
/*
** type for virtual-machine instructions
** must be an unsigned with (at least) 4 bytes
*/
typedef lu_int32 Instruction;
/*
We assume that instructions are unsigned numbers.
All instructions have an opcode in the first 6 bits.
Instructions can have the following fields:
`A' : 8 bits
`B' : 9 bits
`C' : 9 bits
`Bx' : 18 bits (`B' and `C' together)
`sBx' : signed Bx
MSB B C A op LSB
9 9 8 6 bits
*/
根据指令的不同,参数可以表示寄存器的索引,可以表示常量的索引(Proto的TValue *k;数组),可以根据最高位是否是1决定表示寄存器还是常量的索引,还可以是上值的索引(UpVal *upvals[1];),或是其他含义。
寄存器空间不会很大,但常量数组可能会很大,而B、C的大小有限,如果B或C需要引用的常量地址超出了表示范围,在指令生成阶段,则首先会生成指令将常量装载到寄存器,然后再将B或C改为使用该寄存器地址。
常量、上值可以是lua指令的数据来源,寄存器是临时变量,都算是内部数据,那如何与“外部”进行数据交互呢?比如简单的function test() b=a end
,如何读取全局变量a,又赋值给全局变量b。
是通过struct Table *env
,多数情况这个env表就是_G
,可参考 lua源码学习:解释器和内嵌库 load库。
将上面的test函数放到test.lua中,用luac -l -p test.lua看一下test函数的字节码:
0 params, 2 slots, 0 upvalues, 0 locals, 2 constants, 0 functions
1 [1] GETGLOBAL 0 -2 ; a
2 [1] SETGLOBAL 0 -1 ; b
3 [1] RETURN 0 1
constants (2) for 0x563c4dbf3a40:
1 "b"
2 "a"
对比着源代码:
// i是当前指令,ra是A表示的寄存器的位置
case OP_GETGLOBAL: {
TValue g;
sethvalue(L, &g, cl->env);
lua_assert(ttisstring(KBx(i)));
Protect(luaV_gettable(L, &g, KBx(i), ra));
continue;
}
case OP_SETGLOBAL: {
TValue g;
sethvalue(L, &g, cl->env);
lua_assert(ttisstring(KBx(i)));
Protect(luaV_settable(L, &g, KBx(i), ra));
continue;
}
GETGLOBAL,从指令中取出Bx来,将其做为常量索引,取得一个TValue,这个TValue就是TString “a”,然后从_G
中取得名为a的变量的值,放到寄存器上。
SETGLOBAL,ra位置存放的就是getglobal中变量a的值,将其值赋给_G
中名为b的变量。
全局变量名会放到常量表中,如果是局部变量,则只是对应寄存器位置,局部变量的名字除了提供debug信息外,没有其他作用。比如function test() local c=1; b=c end
的字节码:
0 params, 2 slots, 0 upvalues, 2 locals, 2 constants, 0 functions
1 [1] LOADK 0 0 ; 1
2 [1] MOVE 1 0 0
3 [1] SETGLOBAL 0 1 ; d
4 [1] RETURN 0 1 0
constants (2) for 0x55726251aa40:
1 1
2 "d"
locals (2) for 0x55726251aa40:
1 c 2 4
2 b 3 4
LOADK将常量1加载到寄存器0上,MOVE将寄存器0拷贝到寄存器1上,SETGLOBAL将寄存器1的值赋给全局变量b。
关系指令
if a==b then
print("==")
else
print("!=")
end
字节码:
1 [1] GETGLOBAL 0 -1 ; a
2 [1] GETGLOBAL 1 -2 ; b
3 [1] EQ 0 0 1
4 [1] JMP 4 ; to 9
5 [2] GETGLOBAL 0 -3 ; print
6 [2] LOADK 1 -4 ; "=="
7 [2] CALL 0 2 1
8 [2] JMP 3 ; to 12
9 [4] GETGLOBAL 0 -3 ; print
10 [4] LOADK 1 -5 ; "!="
11 [4] CALL 0 2 1
源代码:
// i是当前指令,*pc是下条指令
case OP_EQ: {
TValue *rb = RKB(i);
TValue *rc = RKC(i);
Protect(
if (luaV_equalval(L, rb, rc) == GETARG_A(i))
{
dojump(L, pc, GETARG_sBx(*pc)); // if条件未满足,跳过true的代码段
}
)
pc++;
continue;
}
A的值是0,如果rb不等于rc,pc加上第4条指令中的4,再加1后跳转到第9条指令,就是lua中if不满足的代码;如果rb等于rc,pc加1跳过第4条指令,去执行if满足的代码。
流程大概是这样的:
CMP-------
| |
----TRUE CODE |
| |
| |
| FALSE CODE----
| |
| |
----OTHER CODE
另一种情况:
return a==b
字节码:
1 [1] GETGLOBAL 0 -1 ; a
2 [1] GETGLOBAL 1 -2 ; b
3 [1] EQ 1 0 1
4 [1] JMP 1 ; to 6
5 [1] LOADBOOL 0 0 1
6 [1] LOADBOOL 0 1 0
7 [1] RETURN 0 2
源代码:
case OP_LOADBOOL: {
setbvalue(ra, GETARG_B(i));
if (GETARG_C(i)) pc++; /* skip next instruction (if C) */
continue;
}
A的值是1,相等的情况,跳转到第6条指令,将ra设置为1;不等的情况,跳转到第5条指令,将ra设置为0,此时c为1,会跳过第6条指令。
OP_LT(小于),OP_LE(小于等于),OP_TEST也是类似。
创建和初始化表
创建表时用NEWTABLE创建,哈希部分用SETTABLE初始化,数组部分用SETLIST初始化,比如t={1,2,3}
的字节码是:
0+ params, 4 slots, 0 upvalues, 0 locals, 4 constants, 0 functions
1 [1] NEWTABLE 0 3 0
2 [1] LOADK 1 1 ; 1
3 [1] LOADK 2 2 ; 2
4 [1] LOADK 3 3 ; 3
5 [1] SETLIST 0 3 1 ; 1
6 [1] SETGLOBAL 0 0 ; t
constants (4) for 0x55987156f9c0:
1 "t"
2 1
3 2
4 3
现将3个常数放到寄存器,再用SETLIST设置到表中。如果要创建的表包含很多数组元素,将这些元素放到寄存器时,可能需要很大的寄存器范围。SETLIST实际是分批设置的,每次设置固定数量的元素。
case OP_SETLIST: {
int n = GETARG_B(i);
int c = GETARG_C(i);
int last;
Table *h;
if (n == 0) {
n = cast_int(L->top - ra) - 1;
L->top = L->ci->top;
}
if (c == 0) c = cast_int(*pc++);
if (!ttistable(ra)) break;
h = hvalue(ra);
last = ((c-1)*LFIELDS_PER_FLUSH) + n;
if (last > h->sizearray) /* needs more space? */
luaH_resizearray(L, h, last); /* pre-alloc it at once */
for (; n > 0; n--) {
TValue *val = ra+n;
setobj2t(L, luaH_setnum(L, h, last--), val);
luaC_barriert(L, h, val);
}
continue;
}
以上代码中c就是批次,n是当前批次元素数目。
如果数据量非常大,导致批次超出了C的表示范围,那么C会被设置成0,将SETLIST指令后的指令用来存储批次。
如果使用能产生多个返回值的表达式(… 和 函数调用)初始化表数组项,如果这个表达式不是表构造的最后一项,那么只有第一个值会被使用,其他都会被丢弃;如果是最后一项,那么SETLIST中的B会被设置为0。例如:
function getlist()
return 1,2,3
end
a={getlist()}
function setlist(n,...)
b={...}
end
setlist(4,5,6,7)
closure
参考 upval