Lua虚拟机
1.什么是虚拟机、工作职责?
虚拟机相对于物理机,借助于操作系统对物理机器(CPU等硬件)的一种模拟、抽象,主要扮演CPU和内存的作用。
主要职责:
执行字节码中的指令,管理全局状态(global_state)、数据栈(StackValue)和函数调用链状态(CallInfo)
2.虚拟机分类:基于寄存器和栈VM实现差异?
简单从单条语句来理解:栈VM只有操作码,操作数在栈里,需要从栈获取,处理完结果再次压栈,指令多条;寄存器VM包括操作码和操作数,指令只有一条。简单理解如图:
--test.lua
c = a+b
VM PK_zw.png
由上图可以看出寄存器虚拟机相比减少出入栈的数据拷贝操作和减少生成的指令数量,所以执行效率相对高些。
3.Lua虚拟机如何创建?
通过调用c api 创建luaL_newstate(lauxlib.c),lua_State结构体代表一个Lua虚拟机,可同时创建多个,内部为单线程多实例实现,意味着各自创建的虚拟机栈相互隔离。
创建虚拟机之前,我们先了解下Lua虚拟机体系结构(根据v5.4.3源码整理):
1).创建:lua_state过程(lauxlib.c\lstate.c)
lua_newstate:
LUALIB_API lua_State *luaL_newstate (void) {
lua_State *L = lua_newstate(l_alloc, NULL);
...
return L;
}
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
lua_State *L;
global_State *g;
一系列初始化。。。。
for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
/* memory allocation error: free partial state */
close_state(L);
L = NULL;
}
return L;
}
static void f_luaopen (lua_State *L, void *ud) {
global_State *g = G(L);
UNUSED(ud);
stack_init(L, L); /* init stack */
init_registry(L, g);
luaS_init(L);
luaT_init(L);
luaX_init(L);
g->gcrunning = 1; /* allow gc */
setnilvalue(&g->nilvalue); /* now state is complete */
luai_userstateopen(L);
}
stack_init:初始化栈和初始化调用栈(CallInfo)。
init_registry:初始化注册表,用于注册保存lua中的全局变量和一些C function。
luaS_init: 初始化字符串。
luaT_init:初始化元表元方法相关。
luaX_init: 关键字和一些字符串提前注册并设置不需要GC等。
数据结构:
struct lua_State {
CommonHeader;
lu_byte status;
lu_byte allowhook;
unsigned short nci; /* number of items in 'ci' list */
StkId top; /* first free slot in the stack */
global_State *l_G;
CallInfo *ci; /* call info for current function */
StkId stack_last; /* end of stack (last element + 1) */
StkId stack; /* stack base */
UpVal *openupval; /* list of open upvalues in this stack */
StkId tbclist; /* list of to-be-closed variables */
GCObject *gclist;
struct lua_State *twups; /* list of threads with open upvalues */
struct lua_longjmp *errorJmp; /* current error recover point */
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
volatile lua_Hook hook;
ptrdiff_t errfunc; /* current error handling function (stack index) */
l_uint32 nCcalls; /* number of nested (non-yieldable | C) calls */
int oldpc; /* last pc traced */
int basehookcount;
int hookcount;
volatile l_signalT hookmask;
};
2).Lua二进制文件组成(lua 5.4.3):文件头(31个字节)和函数块
文件头结构:魔数、版本号、格式版本号、int size等,作为文件格式信息,加载脚本文件时都会检查,不符合则拒绝加载。
函数块结构:源代码名、开始/结束行、upvalue/参数数量、指令列表、局部变量表、常量表、函数原型列表等。
如test.lua文件 :
test.lua
a = 1
print(a)
test.lua编译后二进制:(Lua字节码文件:lua文件头和函数体)
1b4c 7561 5400 1993 0d0a 1a0a 0408 0878
5600 0000 0000 0000 0000 0000 2877 4001
8b40 6c75 6133 332e 6c75 6180 8000 0102
8651 0000 000f 8000 010b 0000 0281 0000
8044 0002 0146 0001 0183 0482 6103 0100
0000 0000 0000 0486 7072 696e 7481 0100
0080 8601 0001 0000 0080 8081 855f 454e
56
3).Lua的执行流程:
pro.png
Lua源码文件通过语法词法分析(llex.c/lparser.c)生成Lua的字节码文件(指令集), 再通过Lua虚拟机解析字节码,并执行其中的指令集最后输出结果。
4. 指令集格式
以Lua v5.4.3为例:相对5.3指令有所增加,操作码从6位变成7位[Op(7)], 意味指令的条数增加128条,v5.4.3只定义83条操作指令。lua指令由:操作码和操作数组成。一条指令占32位,操作码固定7位,其余位数分配给操作数; iABC、 iABx,、iAsBx,、iAx,、isJ代表5种指令格式, 简单理解就是指令操作数的组织形式。各自占有位数详见:(lopcodes.h/lopcodes.c 定义)
We assume that instructions are unsigned 32-bit integers.
All instructions have an opcode in the first 7 bits.
Instructions can have the following formats:
3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
iABC C(8) | B(8) |k| A(8) | Op(7) |
iABx Bx(17) | A(8) | Op(7) |
iAsBx sBx (signed)(17) | A(8) | Op(7) |
iAx Ax(25) | Op(7) |
isJ sJ(25) | Op(7) |
enum OpMode {iABC, iABx, iAsBx, iAx, isJ}; /* basic instruction formats */
部分指令集(详见lopnames.h/lopcodes.h ):
OP_MOVE, OP_LOADI, OP_LOADF, OP_LOADK, OP_LOADKX,
OP_LOADFALSE, OP_LFALSESKIP, OP_LOADTRUE,
OP_LOADNIL, OP_GETUPVAL,OP_SETUPVAL, OP_GETTABUP,
OP_GETTABLE, OP_GETI, OP_GETFIELD, OP_SETTABUP,
OP_SETTABLE, OP_SETI, OP_SETFIELD, OP_NEWTABLE,
OP_SELF, OP_ADDI, OP_ADDK, OP_SUBK, OP_MULK,
OP_MODK, OP_ADD, OP_SUB, OP_JMP, OP_EQ,
OP_LT, OP_LE, OP_CALL, OP_TAILCALL, OP_RETURN,
OP_RETURN0, OP_RETURN1, OP_FORLOOP
虚拟机指令主要分为:算术、关系与逻辑、函数调用、upvalue、table、Loop、MOVE与LOAD指令等。
举个栗子:
指令编码0x 00009001解析(32位):
0x9001 = 0000 0000 0000 0000 1001 0000 0000 0001
低7位(0~6)是opcode:000 0001 = 1(OP_LOADI指令),源码中定义指令集为枚举类型,所以0是OP_MOVE,1是OP_LOADI,2是OP_LOADF依次类推。OP_LOADI指令格式是iAsBx。按照上面格式A(A(8) )占用8位, sBx((signed)(17) )为有符号占有17位:
A = 7-15位 为 001 0000 0 = 32(A)
sBx = 14-31位 为0000 0000 0000 0000 1 = 1(sBx)
即指令0x2001 = OP_LOADI 32, 1 :R[A] := sBx(加载1到寄存器R(32)中)。
5.简单lua程序及字节码二进制chunk
test.lua文件 :
test.lua
a = 1
print(a)
test.lua对应的二进制chunk中的所有函数:(luac -l -l test.lua)
main <test.lua:0,0> (6 instructions at 0x7f87714061f0)
0+ params, 2 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
1 [1] VARARGPREP 0
2 [1] SETTABUP 0 0 1k ; _ENV "a" 1
3 [2] GETTABUP 0 0 2 ; _ENV "print"
4 [2] LOADI 1 1
5 [2] CALL 0 2 1 ; 1 in 0 out
6 [2] RETURN 0 1 1 ; 0 out
constants (3) for 0x7f87714061f0:
0 S "a"
1 I 1
2 S "print"
locals (0) for 0x7f87714061f0:
upvalues (1) for 0x7f87714061f0:
0 _ENV 1 0
如上反编译后我们可以看到的内容包括:文件名、起始行数、指令条数及地址、具体指令格式、序号、代码行号、注释、常量、局部变量、upvalues值等。
6. 虚拟机如何执行指令?
1).从C层开始,加载一个Lua脚本是通过宏luaL_dofile来执行一个脚本文件。这个luaL_dofile操作包括luaL_loadfile和lua_pcall两部分组成。源码(lauxlib_h、lvm.c/lvm.h):
#define luaL_dofile(L, fn) \
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
2).通过luaL_loadfile解析Lua脚本文件(二进制或者文本),转换成字节码,过程luaD_protectedparser-->luaD_pcall-->f_parser创建一个lua闭包,然后push到栈中,返回给luaD_protectedparser函数(ldo.c):
luaD_protectedparser function:
status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc);
static void f_parser (lua_State *L, void *ud) {
LClosure *cl;
struct SParser *p = cast(struct SParser *, ud);
int c = zgetc(p->z); /* read first character */
if (c == LUA_SIGNATURE[0]) {
checkmode(L, p->mode, "binary");
cl = luaU_undump(L, p->z, p->name);
}
else {
checkmode(L, p->mode, "text");
cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
}
lua_assert(cl->nupvalues == cl->p->sizeupvalues);
luaF_initupvals(L, cl);
}
3).luaD_call会执行luaD_precall(调用栈信息CallInfo,用于指令执行),最后通过luaV_execute死循环来获取、解析、执行指令。经过以上几步解释器完成从加载文件到执行Chunk的整个流程。
lvm.c#luaV_execute函数如下:
void luaV_execute (lua_State *L, CallInfo *ci) {
。。。
for (;;) {
vmdispatch (GET_OPCODE(i)) {
vmcase(OP_MOVE) {
setobjs2s(L, ra, RB(i));
vmbreak;
}
vmcase(OP_LOADI) {
lua_Integer b = GETARG_sBx(i);
setivalue(s2v(ra), b);
vmbreak;
}
vmcase(OP_LOADF) {
int b = GETARG_sBx(i);
setfltvalue(s2v(ra), cast_num(b));
vmbreak;
}
vmcase(OP_LOADK) {
TValue *rb = k + GETARG_Bx(i);
setobj2s(L, ra, rb);
vmbreak;
}
vmcase(OP_LOADKX) {
TValue *rb;
rb = k + GETARG_Ax(*pc); pc++;
setobj2s(L, ra, rb);
vmbreak;
}
。。。
}
}
}
Lua解析过程_zw.png
7.核心函数:
luaL_loadfile: 编译、解释等。
lua_pcall#luaV_execute: 指令执行(取指,译码,执行;lua_pcall-->luaD_call-->ccall-->luaV_execute[数据结构:CallInfo])
8.核心数据结构:
lua_State、global_state、LClosure、Proto、CallInfo、Instruction等
网友评论