Lua虚拟机深入

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源码整理):

struct_zw.png

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等


学习资料:
Lua官网
参考手册
Bytecode解析: Bytecode Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容