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

参考

探索Lua5.2内部实现

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐