1. Lua虚拟机简介
Lua VM 使用的是 基于寄存器的虚拟机(Register-based)。 指令都是在已经分配好的寄存器中存取操作数。
add a b c 将 寄存器 b 与 寄存器 c 中的值相加,结果存在 寄存器 a 中。 标准的三地址指令,每条指令的表达能力很强,并有效的减少了内存赋值操作。
ADD C,A,B //将A,B寄存器里面的值相加后赋值给C寄存器
除此之外还有一种虚拟机是基于堆栈的虚拟机(Stack-based)。指令都是通过在栈中取数据,所以这类虚拟机在执行指令的时候都会伴随着数据的入栈和出栈。
ILOAD A //将A入栈
ILOAD B //将B入栈
IADD //将栈顶两个元素弹出并相加,结果入栈
ISTORE C //将栈顶弹出赋值给C
由此可见
- 基于堆栈的虚拟机的指令比基于寄存器的指令要小,因为在指令中不需要指定操作数。基于堆栈的虚拟机使用堆栈来保存中间结果、变量等。
- 基于寄存器的虚拟机则支持寄存器的指令操作。基于堆栈的虚拟机需要用Push 、Pop 来传送数据,通常,完成同样的工作,基于寄存器的虚拟机所采用的指令数比基于堆栈的虚拟机采用的指令数目少,可以提高执行效率。
- 堆栈虚拟机指令很低级,基于寄存器的处理器有更强大的指令功能,而且易于调试。 基于堆栈的处理器在处理函数调用、解决递归问题和切换上下文时简单明快。
- 采用寄存器架构时,虚拟机需要经常保存和恢复寄存器中的内容,还要考虑对操作数的寻址问题等,因此,基于堆栈的虚拟机实现起来更简单,基于寄存器的虚拟机能提供更强大的指令集。
下面是Lua虚拟机的一个体系结构简图:
如图所示,实际上虚拟机在执行某个函数原型Proto的时候使用的寄存器R[0]~R[n]对应的还是编译阶段预先分配好的栈空间。只是虚拟机在解释过程中当做了寄存器使用。除了寄存器每个Proto还对应有其解析的instruct指令、constan常量、以及外部的upvalue。
2. Lua虚拟机指令简介
在lua中,用32位的unsigned int类型来表示一条指令操作码,32位值包含了6位的操作码和26位的指令字段两部分内容。
26(高位) | 6(低位) |
---|---|
Instructions | Opcode |
2.1 指令操作码
6位的操作码,所以最多支持 2 6 2^6 26 条指令,在Lua5.3中指令数量为47。我们可以在lopcodes.h文件中可以找到指令的定义:
/*
** R(x) - register
** Kst(x) - constant (in constant table)
** RK(x) == if ISK(x) then Kst(INDEXK(x)) else R(x)
*/
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) - 寄存器的值
** Kst(x) - 常量表的值
** RK(x) - 既可以是寄存器的值也可以常量表的值,其中参数的最高位区分 寄存器索引与常量索引。
*/
2.2 指令格式
高26位的指令字段在不同指令中可能代表不同的情况,其格式定义在lopcodes.h文件中:
enum OpMode {iABC, iABx, iAsBx, iAx}; /* basic instruction format */
在不同的指令格式中,其解释如下表格:
31-23 | 22-14 | 13-6 | OpMode |
---|---|---|---|
B | C | A | iABC |
Bx | A | iABx | |
sBx | A | iAsBX | |
Ax | iAx |
- 每条指令都会对一个对象做出影响,受影响的对象被称为 A。它由 8 bits 来表示。 A 通常是一个寄存器的索引,也可能是对 Upvalue 的操作。
- 作用到 A 的参数一般有两个,每个参数 由 9 bits 表示,分别称为 B 和 C。
- 一部分指令不需要两个操作参数,这时候可以把 B 和 C 合并为一个 18 bits 的整数 Bx 来适应更大的范围。
- 当操作码涉及到跳转指令时,这个参数表示跳转偏移量。向前跳转需要设置偏移量为一个负数。这类指令需要带符号信息来区别,记作 sBx。 其中0被表示为 2^17 ; 1 则表示为 2^17 + 1 ; -1 表示为 2^17 - 1 。
- Lua VM 在运行期,会将需要的常量加载到 寄存器中(Lua 栈),然后利用这些寄存器做相应的工作。 加载常量的操作码 为LOADK,它由两个参数 A ,Bx ,这个操作把Bx 所指的常量加载到 A 所指的寄存器中。 Bx 有 18 bit 长,所以 LOADK 这个操作只能索引到 2^17 个常量。 为了扩大索引常量的上限,提供了LOADKX,它将常量索引号放在了接下来的一条EXTRAARG 指令中。 OP_EXTRAARG 指令 把 opcode所占的 8bit 以外的26 bit 都用于参数表示, 称之为* Ax*。
参数 A、B、C 所占的位数大小以及偏移量 ,在Lua 中由以下一组宏定义:
#define SIZE_C 9
#define SIZE_B 9
#define SIZE_Bx (SIZE_C + SIZE_B)
#define SIZE_A 8
#define SIZE_Ax (SIZE_C + SIZE_B + SIZE_A)
#define SIZE_OP 6
#define POS_OP 0
#define POS_A (POS_OP + SIZE_OP)
#define POS_C (POS_A + SIZE_A)
#define POS_B (POS_C + SIZE_C)
#define POS_Bx POS_C
#define POS_Ax POS_A
A、B、C 用来表示指令操作数的数据来源,在Lua 中 值都存储在三个地方:
- 存在 Lua 寄存器中(也就是 Lua 的数据栈)局部变量。 Lua 使用当前函数的 栈来作为寄存器使用(Lua 寄存器 = Lua 栈),当前函数的栈等同于寄存器数组,即 stack(n) = register(n)。寄存器的idx 从 0 开始。
- 常量表中 ,一般存储存储常量。每一个函数的原型 Proto 都有一个属于本函数的常量表,用于存储编译过程中函数所使用到的常量。常量表可以存放 nil、boolean、number、string类型的数据,常量的idx 从 1 开始。
- 一些既不是常量也不在寄存器的数据 , 存储在 upvalue 表中 或者 Table 表中。每一个函数的原型Proto 中都有一个upvalue 表,用于存储在编译过程中该函数使用的upvalue 。在运行期,通过OP-CLOSURE 指令创建一个 closure时,会根据 Proto 中的描述为这个 closure 初始化upvalue 表。upvalue 也是根据id来索引的。 upvalue 的idx 从 0开始。
2.3 指令格式定义
对于每个操作码的指令格式,在lopcodes.c文件中也有定义:
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 */
};
- T: (第 7 bit) 表示这是不是一条逻辑测试相关的指令,这种指令可能会涉及一次条件跳转,将PC指针自增1。(之所以需要这个标记,是因为Lua 中所有涉及条件分支的地方,实际上都在分支指令后紧随着一条 JMP 指令。Lua 没有 为布尔运算单独设计opcode,它让所有的布尔运算都以分支执行流的形式出现。Lua 的 And 与 Or 关键字 支持短路求值,所以在VM 中以分支跳转的形式实现)。分支指令和之后的 JMP 跳转指令是一体的,是因为32bit 的 Instruction 无法全部描述才分拆为两条指令。这个指令可以用来检测是不是分支指令。 当遇到 JMP 指令时,可以回溯到前面的一条指令来分辨是否是一次条件跳转。 这对 生成Lua 的bytecode 模块有帮助。
- A: (第 6 bit)表示这个指令是否会修改 register A,这个标记在 debug模块被用于跟踪最后改变register 内容的指令位置,帮助生成debug info。
- B : (第 4-5 bit) B arg mode。
- C : (第 2-3 bit) C arg mode。
- opcode:(第 0-1 bit)OpCode的格式,这些分类信息,用于luac 反编译字节码时的输出,对于Lua 的运行时没有实际意义。
因为不是每个指令都需要2-3个参数,所以Lua中定义了一个枚举值OpArgMask来表示参数内存的解释。
enum OpArgMask {
OpArgN, /* 参数没有被使用 */
OpArgU, /* 参数被使用 */
OpArgR, /* 表示寄存器索引或者跳转偏移量 */
OpArgK /* 表示寄存器索引或者常量表索引 */
};
OpArgN 好理解,就是指这个参数对应的内存没有被使用。例如:
opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE A B R(A) := R(B) */
指令格式是iABC,只需要两个参数,所以C参数是没有被使用的。(注意:指令格式只代表每个参数的内存范围,并不表示一定都会用上,也不表示参数的实际含义)
OpArgU 表示这个参数的值就是参数本身(有点绕,区别于OpArgR,OpArgK表示的是索引)
opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_NEWTABLE A B C R(A) := {} (size = B,C) */
OP_NEWTABLE指令创建一个数组部分大小为B,hash表部分大小为C的table放置于寄存器A中;这里可以到:B,C两个参数自身值在指令中作为参数。
OpArgR 表示这个参数是寄存器上的值,为寄存器的索引。
opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE A B R(A) := R(B) */
OP_MOVE表示将B寄存器的值拷贝到A寄存器中,可以看见这里A,B并不表示值本身,而是表示寄存器上索引。
OpArgK 表示这个参数是寄存器上的值也可能是常量表里面的值。
RK(x) == if ISK(x) then Kst(INDEXK(x)) else R(x)
opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_ADD A B C R(A) := RK(B) + RK(C)*/
OP_ADD 表示将B,C 索引所对应的值相加放到寄存器A中,这里可以看到B,C可能是寄存器的索引也可能是常量表里的索引,在Lua中可以用ISK宏来判断是否是寄存器的值。
3. Lua编译指令
在Lua官网下载Lua5.3的源码:Lua5.3源码
将源码添加到工程后,注意添加luac.c不要添加lua.c。编译好luac.exe。
下面是一段简单的Lua代码:
local a = 1
local b = a + 2
print(b)
将Lua代码文件放置于luac.exe同一目录,使用: l u a c . e x e ( 你 编 译 好 后 的 e x e 名 字 ) − l − l m y l u a . l u a ( l u a 文 件 名 ) luac.exe_{(你编译好后的exe名字)} -l -l \quad mylua.lua_{(lua文件名)} luac.exe(你编译好后的exe名字)−l−lmylua.lua(lua文件名)。
luac.exe -l -l mylua.lua
可以得到如下的输出:
main <mylua.lua:0,0> (6 instructions at 007EB310)
0+ params, 4 slots, 1 upvalue, 2 locals, 3 constants, 0 functions
1 [1] LOADK 0 -1 ; 1
2 [2] ADD 1 0 -2 ; - 2
3 [3] GETTABUP 2 0 -3 ; _ENV "print"
4 [3] MOVE 3 1
5 [3] CALL 2 2 1
6 [3] RETURN 0 1
constants (3) for 007EB310:
1 1
2 2
3 "print"
locals (2) for 007EB310:
0 a 2 7
1 b 3 7
upvalues (1) for 007EB310:
0 _ENV 1 0
从上面编译指令可以看到
0+ params, 4 slots, 1 upvalue, 2 locals, 3 constants, 0 functions
0个参数,4个寄存器,1个upvalue,2个局部变量,3个常量,0个函数。
在指令的下面分别是常量表、局部变量表、upvalue表。三个表中第一列都是表中的索引,不过constants的索引在指令中都为对应的负数。
还有值得注意的是:在局部变量表中的第3、4列,分别表示当前局部变量的有效域,比如第0个局部变量a的有效域为第2个指令到第7个指令(上面只有6个指令,说明都有效)。
至于上面的指令含义,我们在第二部分会继续细讲。
4.参考
本博客参考了Bloger TTC的虚拟机指令分析一系列文章: Lua5.3 虚拟机指令分析