Lua源码分析--虚拟机以及指令解释
Lua⼀直把虚拟机执⾏代码的效率作为⼀个⾮常重要的设计⽬标。⽽采⽤什么样的指令系统的对于虚拟机的执⾏效率来说⾄关重要。Stack based vs Register based VM
根据指令获取操作数⽅式的不同,我们可以把虚拟机的实现分为stack based和register based。
Stack based vm
对于⼤多数的虚拟机,⽐如JVM,Python,都采⽤传统的stack based vm。
Stack based vm的指令⼀般都是在当前stack中获取和保存操作数的。⽐如⼀个简单的加法赋值运算:a=b+c,对于stack based vm,⼀般会被转化成如下的指令:
[plain]
1. push b; // 将变量b的值压⼊stack
2. push c; // 将变量c的值压⼊stack
3. add;    // 将stack顶部的两个值弹出后相加,将结果压⼊stack
4. mov a;  // 将stack顶部结果放到a中
由于Stack based vm的指令都是基于当前stack来查操作数的,这就相当于所有操作数的存储位置都是运⾏期决定的,在编译器的代码⽣成阶段不需要额外为在哪⾥存储操作数费⼼,所以stack based的编译器实现起来相对⽐较简单直接。也正因为这个原因,每条指令占⽤的存储空间也⽐较⼩。
但是,对于⼀个简单的运算,stack based vm会使⽤过多的指令组合来完成,这样就增加了整体指令集合的长度。vm会使⽤同样多的迭代次数来执⾏这些指令,这对于效率来说会有很⼤的影响。并且,由于操作数都要放到stack上⾯,使得移动这些操作数的内存复制⼤⼤增加,这也会影响到效率。
Register based vm
Lua 采⽤的是register based vm。
Register based vm的指令都是在已经分配好的寄存器中存取操作数。对于上⾯的运算,register based vm⼀般会使⽤如下的指令:
[plain]
1. add a b c; // 将b与c对应的寄存器的值相加,将结果保存在a对应的寄存器中
Register based vm的指令可以直接对应标准的3地址指令,⽤⼀条指令完成了上⾯多条指令的计算⼯作,并且有效地减少了内存复制操作。这样的指令系统对于效率有很⼤的帮助。
不过,在编译器设计上,就要在代码⽣成阶段对寄存器进⾏分配,增加了实现的复杂度。并且每条指令所占⽤的存储空间也相应的增加了。Lua虚拟机指令简介
Lua的指令使⽤⼀个32bit的unsigned integer表⽰。所有指令的定义都在lopcodes.h⽂件中,使⽤⼀个enum OpCode代表指令类型。在lua5.2中,总共有40种指令(id从0到39)。根据指令参数的不同,可以将所有指令分为4类:
除了sBx之外,所有的指令参数都是unsigned integer类型。sBx可以表⽰负数,但表⽰⽅法⽐较特殊。sBx的18bit可表⽰的最⼤整数为262143,这个数的⼀半131071⽤来表⽰0,所以-1可以表⽰为-1+131071,也就是131070,⽽+1可以表⽰为+1+131071,也就是131072。
ABC⼀般⽤来存放指令操作数据的地址,⽽地址可以分成3种:
Lua使⽤当前函数的stack作为寄存器使⽤,寄存器id从0开始。当前函数的stack与寄存器数组是相同的概念。stack(n)其实就是register(n)。
每⼀个函数prototype都有⼀个属于本函数的常量表,⽤于存放编译过程中函数所⽤到的常量。常量表可以存放nil,boolean,number和string类型的数据,id从1开始。
每⼀个函数prototype中都有⼀个upvalue描述表,⽤于存放在编译过程中确定的本函数所使⽤的upvalue的描述。在运⾏期,通过
OP_CLOSURE指令创建⼀个closure时,会根据prototype中的描述,为这个closure初始化upvalue表。upvalue本⾝不需要使⽤名称,⽽是通过id进⾏访问。
A被⼤多数指令⽤来指定计算结果的⽬标寄存器地址。很多指令使⽤B或C同时存放寄存器地址和常量地址,并通过最左⾯的⼀个bit来区分。在指令⽣成阶段,如果B或C需要引⽤的常量地址超出了表⽰范围,则⾸先会⽣成指令将常量装载到临时寄存器,然后再将B或C改为使⽤该寄存器地址。
在lopcodes.h中,对于每个指令,在源码注释中都有简单的操作描述。本⽂接下来将针对每⼀个指令做更详细的描述,并给出关于这个指令的⽰例代码。⽰例代码可以帮助我们构建出⼀个指令使⽤的具体上下⽂,有助于进⼀步理解指令的作⽤。对指令上下⽂的理解还可以作为进⼀步研究lua的编译和代码⽣成系统的基础。
在分析过程中,我们使⽤luac来显⽰⽰例代码所⽣成的指令。luac的具体使⽤⽅式为:
[plain]
1. luac -l -l test.lua
Lua⾸先将源程序编译成为字节码,然后交由虚拟机解释执⾏.对于每⼀个函数,Lua的编译器将创建⼀个原型(prototype),它由⼀组指令及其使⽤到的常量组成[1].最初的Lua虚拟机是基于栈的.到1993年,Lua5.0版本,采⽤了基于寄存器的虚拟机,使得Lua的解释效率得到提升,
1、指令系统
与虚拟机和指令相关的⽂件主要有两个: lopcodes.c和从名称可以看出来,这两个⽂件分别⽤于描述操作码(指令)和虚拟机.
(1)指令列举
Lua共有38条指令,在下⾯两处地⽅分别描述了这些指令的名称和模式,如下:
lopcodes.c:16
const char*const luaP_opnames[NUM_OPCODES+1] = {
"MOVE",
"LOADK",
"LOADBOOL",
"LOADNIL",
"GETUPVAL",
"GETGLOBAL",
"GETTABLE",
"SETGLOBAL",
"SETUPVAL",
"SETTABLE",
"NEWTABLE",
"SELF",
"ADD",
"SUB",
"MUL",
"DIV",
"MOD",
"POW",
"UNM",
"NOT",
"LEN",
"CONCAT",
"JMP",
"EQ",
"LT",
"LE",
"TEST",
"TESTSET",
"CALL",
"TAILCALL",
"RETURN",
"FORLOOP",
"FORPREP",
"TFORLOOP",
"SETLIST",
"CLOSE",
"CLOSURE",
"VARARG",
NULL
};
#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) |((c)<<2) | (m))
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, OpArgU, OpArgU, iABC)      /* OP_LOADBOOL */ ,opmode(0, 1, OpArgR, OpArgN, iABC)      /* OP_LOADNIL */
,opmode(0, 1, OpArgU, OpArgN, iABC)      /* OP_GETUPVAL */ ,opmode(0, 1, OpArgK, OpArgN, iABx)      /* OP_GETGLOBAL */ ,opmode(0, 1, OpArgR, OpArgK, iABC)      /* OP_GETTABLE */
,opmode(0, 0, OpArgK, OpArgN, iABx)      /* OP_SETGLOBAL */ ,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_DIV */
,opmode(0, 1, OpArgK, OpArgK, iABC)      /* OP_MOD */
,opmode(0, 1, OpArgK, OpArgK, iABC)      /* OP_POW */
,opmode(0, 1, OpArgR, OpArgN, iABC)      /* OP_UNM */
,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, 1, OpArgR, 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(1, 0, OpArgN, OpArgU, iABC)      /* OP_TFORLOOP */ ,opmode(0, 0, OpArgU, OpArgU, iABC)      /* OP_SETLIST */
,opmode(0, 0, OpArgN, OpArgN, iABC)      /* OP_CLOSE */
,opmode(0, 1, OpArgU, OpArgN, iABx)      /* OP_CLOSURE */
,opmode(0, 1, OpArgU, OpArgN, iABC)      /* OP_VARARG */
};
前⾯⼀个数组容易理解,表⽰了每条指令的名称.后⾯⼀个数组表⽰的是指令的模式.奇怪的符号让⼈有些费解.在看模式之前,⾸先来看Lua指令的格式:
如上图,Lua的指令可以分成三种形式.即在上⾯的模式数组中也可以看到的iABC, iABx和 iAsBx.对于三种形式的指令来说,前两部分都是⼀样的,分别是6位的操作码和8位A操作数;区别在于,后⾯部是分割成为两个长度为9位的操作符(B, C),⼀个⽆符号的18位操作符Bx还是有符号的18位操作符sBx.这些定义的代码如下:
lopcodes.c : 34
/*
** size and position of opcode arguments.
*/
#define SIZE_C        9
#define SIZE_B        9
#define SIZE_Bx        (SIZE_C + SIZE_B)
#define SIZE_A        8
#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
(2)指令的操作模式
Lua使⽤⼀个字节来表⽰指令的操作模式.具体的含义如下:
1)使⽤最⾼位来表⽰是否是⼀条测试指令.之所以将这⼀类型的指令特别地标识出来,是因为Lua的指令长度是32位,对于分⽀指令来说,要想在这32位中既表⽰两个操作数来做⽐较,同时还要表⽰⼀个跳转的地址,是很困难的.
python虚拟机
因此将这种指令分成两条,第⼀条是测试指令,紧接着⼀条⽆条件跳转.如果判断条件成⽴则将PC(Program Counter,指⽰下⼀条要执⾏的指令)加⼀,跳过下⼀条⽆条件跳转指令,继续执⾏;否则跳转.
2)第⼆位⽤于表⽰A操作数是否被设置
3) 接下来的⼆位⽤于表⽰操作数B的格式,OpArgN表⽰操作数未被使⽤, OpArgU表⽰操作数被使⽤(⽴即数?), OpArgR表⽰表⽰操作数是寄存器或者跳转的偏移量, OpArgK表⽰操作数是寄存器或者常量.
2、Lua虚拟机的体系结构
给出Lua虚拟机的体系结构图(根据源代码分析得出):
⾸先,我们注意到,Lua的解释器还是⼀个以栈为中⼼的结构.
在lua_State这个结构中,有许多个字段⽤于描述这个结构.stack⽤于指向绝对栈底,⽽base指向了当前正在执⾏的函数的第⼀个参数,⽽top指向栈顶的第⼀个空元素.
我们可以看到,这个体系结构中并没有独⽴出来的寄存器.
从以下代码来看:
lvm.c:343
#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)    check_exp(getBMode(GET_OPCODE(i)) == OpArgK,k+GETARG_Bx(i))
当指令操作数的类型是寄存器时,它的内容是以base为基址在栈上的索引值.如图所⽰.寄存器实际是base之上栈元素的别名;当指令操作数的类型的常数时,它⾸先判断B操作数的最位是否为零.如果是零,则按照和寄存器的处理⽅法⼀样做,如果不是零,则在常数表中相应的值.
我们知道Lua中函数的执⾏过程是这样的.⾸先将函数压栈,然后依次将参数压栈,形成图中所⽰的栈的内容.因此R[0]到R[n]也分别表⽰
了Arg[1]到Arg[N+1].在第⼀个参数之下,就是当前正在执⾏的函数,对于Lua的函数(相对C函数)来说,它是指向类型
为 Prototype的TValue,在Prototype中字段code指向了⼀个数组⽤来表⽰组成这个函数的所有指令,字段k指向⼀个数组来表⽰这个函数使⽤到的所有常量.最后,Lua在解释执⾏过程中有专门的变量pc来指向下⼀条要执⾏的指令.
3、指令解释
有了前⾯对指令格式和体系结构的介绍,
现在我们可以进⼊正题,来看看Lua的指令是如何执⾏的了.主函数如下:
(1)执⾏lua指令函数

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。