Lua VM 使用的是 Register based VM。 指令都是在已经分配好的寄存器中存取操作数。
add a b c 将 寄存器 b 与 寄存器 c 中的值相加,结果存在 寄存器 a 中。 标准的三地址指令,每条指令的表达能力很强,并有效的减少了内存赋值操作。
#if LUAI_BITSINT >= 32
typedef unsigned int Instruction;
#else
typedef unsigned long Instruction;
#endif
Lua 的指令使用一个 32bit 的无符号整数表示,其中低 6 位 表示操作码,所以最多支持 2 ^ 6 = 64 条指令。所有的指令定义如下:
typedef enum {
/*----------------------------------------------------------------------
name args description
------------------------------------------------------------------------*/
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) */
OP_NEWTABLE,/* A B C R(A) := {} (size = B,C) */
OP_SELF,/* A B C R(A+1) := R(B); R(A) := R(B)[RK(C)] */
OP_ADD,/* A B C R(A) := RK(B) + RK(C) */
OP_SUB,/* A B C R(A) := RK(B) - RK(C) */
OP_MUL,/* A B C R(A) := RK(B) * RK(C) */
OP_MOD,/* A B C R(A) := RK(B) % RK(C) */
OP_POW,/* A B C R(A) := RK(B) ^ RK(C) */
OP_DIV,/* A B C R(A) := RK(B) / RK(C) */
OP_IDIV,/* A B C R(A) := RK(B) // RK(C) */
OP_BAND,/* A B C R(A) := RK(B) & RK(C) */
OP_BOR,/* A B C R(A) := RK(B) | RK(C) */
OP_BXOR,/* A B C R(A) := RK(B) ~ RK(C) */
OP_SHL,/* A B C R(A) := RK(B) << RK(C) */
OP_SHR,/* A B C R(A) := RK(B) >> RK(C) */
OP_UNM,/* A B R(A) := -R(B) */
OP_BNOT,/* A B R(A) := ~R(B) */
OP_NOT,/* A B R(A) := not R(B) */
OP_LEN,/* A B R(A) := length of R(B) */
OP_CONCAT,/* A B C R(A) := R(B).. ... ..R(C) */
OP_JMP,/* A sBx pc+=sBx; if (A) close all upvalues >= R(A - 1) */
OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */
OP_LT,/* A B C if ((RK(B) < RK(C)) ~= A) then pc++ */
OP_LE,/* A B C if ((RK(B) <= RK(C)) ~= A) then pc++ */
OP_TEST,/* A C if not (R(A) <=> C) then pc++ */
OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */
OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */
OP_RETURN,/* A B return R(A), ... ,R(A+B-2) (see note) */
OP_FORLOOP,/* A sBx R(A)+=R(A+2);
if R(A) = R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/* A sBx R(A)-=R(A+2); pc+=sBx */
OP_TFORCALL,/* A C R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2)); */
OP_TFORLOOP,/* A sBx if R(A+1) ~= nil then { R(A)=R(A+1); pc += sBx }*/
OP_SETLIST,/* A B C R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B */
OP_CLOSURE,/* A Bx R(A) := closure(KPROTO[Bx]) */
OP_VARARG,/* A B R(A), R(A+1), ..., R(A+B-2) = vararg */
OP_EXTRAARG/* Ax extra (larger) argument for previous opcode */
} OpCode;
注释说明了该指令操作方式
/*
** R(x) - register
** Kst(x) - constant (in constant table)
** RK(x) == if ISK(x) then Kst(INDEXK(x)) else R(x)
*/
R()表示这一定是 寄存器索引(一定要操作Lua 栈)
RK()表示这有可能是 一个寄存器索引 或 是一个常量索引,RK 只能用 参数B 与 参数 C (SIZE_B = SIZE_C = 9 ),其中参数的最高位区分 寄存器索引与常量索引。
下面这组宏提供一些操作:
/*
** Macros to operate RK indices
*/
/* this bit 1 means constant (0 means register) */
#define BITRK (1 << (SIZE_B - 1))
/* test whether value is a constant */
#define ISK(x) ((x) & BITRK)
/* gets the index of the constant */
#define INDEXK(r) ((int)(r) & ~BITRK)
#define MAXINDEXRK (BITRK - 1)
/* code a constant index as a RK value */
#define RKASK(x) ((x) | BITRK)
Lua VM 在运行期,会将需要的常量加载到 寄存器中(Lua 栈),然后利用这些寄存器做相应的工作。 加载常量的操作码 为LOADK,它由两个参数 A ,Bx ,这个操作把Bx 所指的常量加载到 A 所指的寄存器中。 Bx 有 18 bit 长,所以 LOADK 这个操作只能索引到 2^18 个常量。 为了扩大索引常量的上限,提供了LOADKX,它将常量索引号放在了接下来的一条EXTRAARG 指令中。 OP_EXTRAARG 指令 把 opcode所占的 8bit 以外的26 bit 都用于参数表示, 称之为* Ax*。
参数 A、B、C 所占的位数大小以及偏移量 ,在Lua 中由以下一组宏定义:
/*
** size and position of opcode arguments.
*/
参数 A、B、C 一般用来存放指令操作数据的地址(索引),而地址(索引)有以下三种:
1. 寄存器 idx
2. 常量表 idx
3. upvalue idx
个人理解:
- Lua VM 是基于寄存器结构实现的,也就是说:每一段Lua chunk 代码都可以看成 被翻译成的一组对 256 (Opcode 只有 8 bit 2^8=256)个寄存器的操作指令。
- 我们在用C 语言给Lua 编写扩展的时候,C 函数通常是从lua-State 中取出参数逐个记录在 C 的局部变量中, 然后利用 C 代码直接对这些值进行操作。
- 在Lua 中 值都存储在三个地方:\<1>存在 Lua 寄存器中(也就是 Lua 的数据栈)局部变量。 \<2> 常量表中 ,一般存储存储常量。\<3> 一些既不是常量也不在寄存器的数据 , 存储在 upvalue 表中 或者 Table 表中。 可以看出来 OpCode 参数 A、B、C 存储的操作数据的索引根据不同的指令在三种不同的存储位置(寄存器、常量表、upvalue表)来获取值。
- Lua 使用当前函数的 栈来作为寄存器使用(Lua 寄存器 = Lua 栈),寄存器的idx 从 0 开始。当前函数的栈等同于寄存器数组,即 stack(n) = register(n)。
- 每一个函数的原型 Proto 都有一个属于本函数的常量表,用于存储编译过程中函数所使用到的常量。常量表可以存放 nil、boolean、number、string类型的数据,常量的idx 从 1 开始。
- 每一个函数的原型Proto 中都有一个upvalue 表,用于存储在编译过程中该函数使用的upvalue 。在运行期,通过OP-CLOSURE 指令创建一个 closure时,会根据 Proto 中的描述为这个 closure 初始化upvalue 表。upvalue 也是根据id来索引的。 upvalue 的idx 从 0开始。
TTcs-Mac-mini:OpCode ttc$ cat tOP_VALUE.lua
local a = 100
function foo()
local b = 200
a = 300
c = b
end
TTcs-Mac-mini:OpCode ttc$ ./luac -l -l tOP_VALUE.lua
main (4 instructions at 0x7f92e44039b0)
0+ params, 2 slots, 1 upvalue, 1 local, 2 constants, 1 function
1 [1] LOADK (iABx) [A]0 [K]-1 ; 100
2 [7] CLOSURE (iABx) [A]1 [U]0 ; 0x7f92e4403b70
3 [3] SETTABUP (iABC) [A]0 [ISK]256[B]-2[ISK]0[C]1 ; _ENV "foo"
4 [7] RETURN (iABC) [A]0 [ISK]0[B]1[ISK]0
constants (2) for 0x7f92e44039b0:
1(idx) 100
2(idx) "foo"
locals (1) for 0x7f92e44039b0:
0 a(name) 2(startpc) 5(endpc)
upvalues (1) for 0x7f92e44039b0:
0 _ENV(name) 1(instack) 0(idx)
function (5 instructions at 0x7f92e4403b70)
0 params, 2 slots, 2 upvalues, 1 local, 3 constants, 0 functions
1 [4] LOADK (iABx) [A]0 [K]-1 ; 200
2 [5] LOADK (iABx) [A]1 [K]-2 ; 300
3 [5] SETUPVAL (iABC) [A]1 [ISK]0[B]0[ISK]0 ; a
4 [6] SETTABUP (iABC) [A]1 [ISK]256[B]-3[ISK]0[C]0 ; _ENV "c"
5 [7] RETURN (iABC) [A]0 [ISK]0[B]1[ISK]0
constants (3) for 0x7f92e4403b70:
1(idx) 200
2(idx) 300
3(idx) "c"
locals (1) for 0x7f92e4403b70:
0 b(name) 2(startpc) 6(endpc)
upvalues (2) for 0x7f92e4403b70:
0 a(name) 1(instack) 0(idx)
1 _ENV(name) 0(instack) 0(idx)
TTcs-Mac-mini:OpCode ttc$ ''
Lua 代码 a 是局部变量 , 在 foo 函数中,a 是闭包变量(upvalue),c 是全局变量(_ENV 表),可以看出三个不同地方存储值的索引起始编号。
目前Lua 5.3 使用了47个OpCode ,分为了 4种不同的 mode类型:iABC , iABx , iAsBx , iAx。 Lua 用一个数组定义所有OpCode的一些说明信息:
#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m))
LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {
/* T A B C mode opcode */
opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE */
,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_LOADK */
,opmode(0, 1, OpArgN, OpArgN, iABx) /* OP_LOADKX */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_LOADBOOL */
,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_LOADNIL */
,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_GETUPVAL */
,opmode(0, 1, OpArgU, OpArgK, iABC) /* OP_GETTABUP */
,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_GETTABLE */
,opmode(0, 0, OpArgK, OpArgK, iABC) /* OP_SETTABUP */
,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_SETUPVAL */
,opmode(0, 0, OpArgK, OpArgK, iABC) /* OP_SETTABLE */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_NEWTABLE */
,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_SELF */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_ADD */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SUB */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MUL */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MOD */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_POW */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_DIV */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_IDIV */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_BAND */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_BOR */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_BXOR */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SHL */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SHR */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_UNM */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_BNOT */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_NOT */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LEN */
,opmode(0, 1, OpArgR, OpArgR, iABC) /* OP_CONCAT */
,opmode(0, 0, OpArgR, OpArgN, iAsBx) /* OP_JMP */
,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_EQ */
,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LT */
,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LE */
,opmode(1, 0, OpArgN, OpArgU, iABC) /* OP_TEST */
,opmode(1, 1, OpArgR, OpArgU, iABC) /* OP_TESTSET */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_CALL */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_TAILCALL */
,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_RETURN */
,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORLOOP */
,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORPREP */
,opmode(0, 0, OpArgN, OpArgU, iABC) /* OP_TFORCALL */
,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_TFORLOOP */
,opmode(0, 0, OpArgU, OpArgU, iABC) /* OP_SETLIST */
,opmode(0, 1, OpArgU, OpArgN, iABx) /* OP_CLOSURE */
,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_VARARG */
,opmode(0, 0, OpArgU, OpArgU, iAx) /* OP_EXTRAARG */
};
这里用一个宏opmode封装了每个OpCode的具体格式,其中:
1. T: (第 7 bit) 表示这是不是一条逻辑测试相关的指令,这种指令可能会涉及一次条件跳转,将PC指针自增1。(之所以需要这个标记,是因为Lua 中所有涉及条件分支的地方,实际上都在分支指令后紧随着一条 JMP 指令。Lua 没有 为布尔运算单独设计opcode,它让所有的布尔运算都以分支执行流的形式出现。Lua 的 And 与 Or 关键字 支持短路求值,所以在VM 中以分支跳转的形式实现)。分支指令和之后的 JMP 跳转指令是一体的,是因为32bit 的 Instruction 无法全部描述才分拆为两条指令。这个指令可以用来检测是不是分支指令。 当遇到 JMP 指令时,可以回溯到前面的一条指令来分辨是否是一次条件跳转。 这对 生成Lua 的bytecode 模块有帮助。
2. A: (第 6 bit)表示这个指令是否会修改 register A,这个标记在 debug模块被用于跟踪最后改变register 内容的指令位置,帮助生成debug info。
3. B : (第 4-5 bit) B arg mode。
4. C : (第 2-3 bit) C arg mode。
4. mode:(第 0-1 bit)OpCode的格式,这些分类信息,用于luac 反编译字节码时的输出,对于Lua 的运行时没有实际意义。
Lua代码中提供下面一组宏, 用来根据 Opcode 参数 A、B、C 中的值(索引)来获取相应 Lua 值 (Lua VM 执行再详细分析)
#define RA(i) (base+GETARG_A(i))
/* to be used after possible stack reallocation */
#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))
#define KBx(i) \
(k + (GETARG_Bx(i) != 0 ? GETARG_Bx(i) - 1 : GETARG_Ax(*ci->u.l.savedpc++)))