lua函数执行和虚拟机指令

Stack based vs Register based VM

可直接参考 Stack based vs Register based VM

lua函数调用

先看一下lua函数的结构:

c 复制代码
/*
** 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数组。

c 复制代码
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"

对比着源代码:

c 复制代码
  // 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。

关系指令

lua 复制代码
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

源代码:

c 复制代码
  // 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

另一种情况:

lua 复制代码
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

源代码:

c 复制代码
  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实际是分批设置的,每次设置固定数量的元素。

c 复制代码
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。例如:

lua 复制代码
function getlist()
	return 1,2,3
end
a={getlist()}
function setlist(n,...)
	b={...}
end
setlist(4,5,6,7)

closure

参考 upval

参考

探索Lua5.2内部实现

相关推荐
夜猫逐梦18 小时前
【lua】二进制数据打包和解析
lua
夜猫逐梦2 天前
【lua】table基础操作
lua
大飞pkz4 天前
【Lua】题目小练12
开发语言·lua·题目小练
大得3694 天前
国产nginx,tengine,内部已有lua,未安装mysql,安装mysql
运维·nginx·lua
大得3695 天前
nginx结合lua做转发,负载均衡
nginx·junit·lua
SimpleUmbrella6 天前
windows下配置lua环境
c++·lua
朱砂绛6 天前
【大模型本地运行与部署框架】Ollama的API交互
开发语言·lua·交互
郭京京8 天前
go语言redis中使用lua脚本
redis·go·lua
Teamol20208 天前
Lua 语法核心特点
单元测试·lua