详解关于Lua源码分析学习教程
关于Lua源码分析学习教程是本文要介绍的内容,主要来了解LUA中源码的使用方法。Lua首先将源程序编译成为字节码,然后交由虚拟机解释执行.对于每一个函数,Lua的编译器将创建一个原型(prototype),它由一组指令及其使用到的常量组成.最初的Lua虚拟机是基于栈的.到1993年,Lua5.0版本,采用了基于寄存器的虚拟机,使得Lua的解释效率得到提升,体系结构与指令系统
与虚拟机和指令相关的文件主要有两个: lopcodes.c 和 lvm.c. 从名称可以看出来,这两个文件分别用于描述操作码(指令)和虚拟机.
首先来看指令:
Lua共有38条指令, 在下面两处地方分别描述了这些指令的名称和模式, 如下:
[*]lopcodes.c:16
[*]const char *const luaP_opnames = {
[*]"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 = {
[*]/* 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指令的格式,如图:
http://images.51cto.com/files/uploadimg/20110825/1548050.jpg
如上图, 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
再来看指令的操作模式, Lua使用一个字节来表示指令的操作模式. 具体的含义如下:
1、使用最高位来表示是否是一条测试指令. 之所以将这一类型的指令特别地标识出来, 是因为Lua的指令长度是32位,对于分支指令来说, 要想在这32位中既表示两个操作数来做比较, 同时还要表示一个跳转的地址, 是很困难的. 因此将这种指令分成两条, 第一条是测试指令, 紧接着一条无条件跳转. 如果判断条件成立则将PC(Program Counter, 指示下一条要执行的指令)加一, 跳过下一条无条件跳转指令, 继续执行; 否则跳转.
2、第二位用于表示A操作数是否被设置
3、接下来的二位用于表示操作数B的格式,OpArgN表示操作数未被使用, OpArgU表示操作数被使用(立即数?), OpArgR表示表示操作数是寄存器或者跳转的偏移量, OpArgK表示操作数是寄存器或者常量.
最后, 给出Lua虚拟机的体系结构图(根据源代码分析得出):
http://images.51cto.com/files/uploadimg/20110825/1548051.jpg
首先, 我们注意到, 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到R也分别表示了Arg到Arg.在第一个参数之下,就是当前正在执行的函数,对于Lua的函数(相对C函数)来说,它是指向类型为 Prototype的TValue, 在Prototype中字段code指向了一个数组用来表示组成这个函数的所有指令,字段k指向一个数组来表示这个函数使用到的所有常量.最后,Lua在解释执行过程中有专门的变量pc来指向下一条要执行的指令.
指令解释器
有了前面对指令格式和体系结构的介绍,现在我们可以进入正题, 来看看Lua的指令是如何执行的了.主函数如下:
[*]lvm.c:373
[*]void luaV_execute (lua_State *L, int nexeccalls) {
[*]LClosure *cl;
[*]StkId base;
[*]TValue *k;
[*]const Instruction *pc;
[*]reentry: /* entry point */
[*]lua_assert(isLua(L->ci));
[*]pc = L->savedpc;
[*]cl = &clvalue(L->ci->func)->l;
[*]base = L->base;
[*]k = cl->p->k;
这是最开始的初始化过程.其中, pc被初始化成为了L->savedpc,base被初始化成为了L->base, 即程序从L->savedpc开始执行 (在下一篇专题中,将会介绍到 L->savedpc在函数调用的预处理过程中指向了当前函数的code),而L->base指向栈中当前函数的下一个位置.cl表示当前正在执行闭包(当前可以理解成为函数),k指向当前闭包的常量表.
接下来(注意,为了专注主要逻辑, 我将其中用于Debugger支持,断言等代码省略了):
[*]/* main loop of interpreter */
[*]for (;;) {
[*] const Instruction i = *pc++;
[*] StkId ra;
[*] /* 省略Debugger支持和Coroutine支持*/
[*] /* warning!! several calls may realloc the stack and invalidate `ra' */
[*] ra = RA(i);
[*] /* 省略断言 */
[*] switch (GET_OPCODE(i)) {
进入到解释器的主循环,处理很简单,取得当前指令,pc递增,初始化ra,然后根据指令的操作码进行选择. 接下来的代码是什么样的, 估计大家都能想到,一大串的case来指示每条指令的执行.具体的实现可以参考源码, 在这里不对每一条指令展开, 只是对其中有主要的几类指令进行说明:
传值类的指令,与MOVE为代表:
[*]lvm.c:403
[*] case OP_MOVE: {
[*] setobjs2s(L, ra, RB(i));
[*] continue;
[*] }
[*]lopcodes:154
[*]OP_MOVE,/* A B R(A) := R(B) */
[*]lobject.h:161
[*]#define setobj(L,obj1,obj2) \
[*]{ const TValue *o2=(obj2); TValue *o1=(obj1); \
[*] o1->value = o2->value; o1->tt=o2->tt; \
[*] checkliveness(G(L),o1); }
[*]
[*]/*
[*]** different types of sets, according to destination
[*]*/
[*]/* from stack to (same) stack */
[*]#define setobjs2s setobj
从注释来看, 这条指令是将操作数A,B都做为寄存器,然后将B的值给A. 而实现也是简单明了,只使用了一句. 宏展开以后, 可以看到, R,R的类型是TValue, 只是将这两域的值传过来即可. 对于可回收对象来说,真实值不会保存在栈上,所以只是改了指针,而对于非可回收对象来说,则是直接将值从R赋到R.
数值运算类指令,与ADD为代表:
[*]lvm.c:470
[*] case OP_ADD: {
[*] arith_op(luai_numadd, TM_ADD);
[*] continue;
[*] }
[*]lvm.c:360
[*]#define arith_op(op,tm) { \
[*] TValue *rb = RKB(i); \
[*] TValue *rc = RKC(i); \
[*] if (ttisnumber(rb) && ttisnumber(rc)) { \
[*] lua_Number nb = nvalue(rb), nc = nvalue(rc); \
[*] setnvalue(ra, op(nb, nc)); \
[*] } \
[*] else \
[*] Protect(Arith(L, ra, rb, rc, tm)); \
[*] }
[*]lopcodes.c:171
[*]OP_ADD,/* A B C R(A) := RK(B) + RK(C) */
如果两个操作数都是数值的话,关键的一行是:
[*]setnvalue(ra,op(nb,nc));
即两个操作数相加以后,把值赋给R.值得注意的是,操作数B,C都是RK, 即可能是寄存器也可能是常量,这最决于最B和C的最高位是否为1,如果是1,则是常量,反之则是寄存器.具体可以参考宏ISK的实现.
如果两个操作数不是数值,即调用了Arith函数,它尝试将两个操作转换成数值进行计算,如果无法转换,则使用元表机制.该函数的实现如下:
[*]lvm.c:313
[*]static void Arith (lua_State *L, StkId ra, const TValue *rb,
[*] const TValue *rc, TMS op) {
[*]TValue tempb, tempc;
[*]const TValue *b, *c;
[*]if ((b = luaV_tonumber(rb, &tempb)) != NULL &&
[*] (c = luaV_tonumber(rc, &tempc)) != NULL) {
[*] lua_Number nb = nvalue(b), nc = nvalue(c);
[*] switch (op) {
[*] case TM_ADD: setnvalue(ra, luai_numadd(nb, nc)); break;
[*] case TM_SUB: setnvalue(ra, luai_numsub(nb, nc)); break;
[*] case TM_MUL: setnvalue(ra, luai_nummul(nb, nc)); break;
[*] case TM_DIV: setnvalue(ra, luai_numdiv(nb, nc)); break;
[*] case TM_MOD: setnvalue(ra, luai_nummod(nb, nc)); break;
[*] case TM_POW: setnvalue(ra, luai_numpow(nb, nc)); break;
[*] case TM_UNM: setnvalue(ra, luai_numunm(nb)); break;
[*] default: lua_assert(0); break;
[*] }
[*]}
[*]else if (!call_binTM(L, rb, rc, ra, op))
[*] luaG_aritherror(L, rb, rc);
[*]}
在上面call_binTM用于调用到元表中的元方法,因为在Lua以前的版本中元方法也被叫做tag method, 所以函数最后是以TM结尾的.
[*]lvm:163
[*]static int call_binTM (lua_State *L, const TValue *p1, const TValue *p2,
[*] StkId res, TMS event) {
[*]const TValue *tm = luaT_gettmbyobj(L, p1, event); /* try first operand */
[*]if (ttisnil(tm))
[*] tm = luaT_gettmbyobj(L, p2, event); /* try second operand */
[*]if (!ttisfunction(tm)) return 0;
[*]callTMres(L, res, tm, p1, p2);
[*]return 1;
[*]}
在 这个函数中,试着从二个操作数中找到其中一个操作数的元方法(第一个操作数优先), 这里event表示具体哪一个元方法,找到了之后,再使用函数callTMres()去调用相应的元方法. callTMres()的实现很简单,只是将元方法,第一,第二操作数先后压栈,再调用并取因返回值.具体如下:
[*]lvm.c:82
[*]static void callTMres (lua_State *L, StkId res, const TValue *f,
[*] const TValue *p1, const TValue *p2) {
[*]ptrdiff_t result = savestack(L, res);
[*]setobj2s(L, L->top, f); /* push function */
[*]setobj2s(L, L->top+1, p1); /* 1st argument */
[*]setobj2s(L, L->top+2, p2); /* 2nd argument */
[*]luaD_checkstack(L, 3);
[*]L->top += 3;
[*]luaD_call(L, L->top - 3, 1);
[*]res = restorestack(L, result);
[*]L->top--;
[*]setobjs2s(L, res, L->top);
[*]}
逻辑运算类指令,与EQ为代表:
[*]lvm.c:541
[*] case OP_EQ: {
[*] TValue *rb = RKB(i);
[*] TValue *rc = RKC(i);
[*] Protect(
[*] if (equalobj(L, rb, rc) == GETARG_A(i))
[*] dojump(L, pc, GETARG_sBx(*pc));
[*] )
[*] pc++;
[*] continue;
[*] }
[*]lopcodes.c:185
[*]OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */
在 这条指令实现的过程中,equalobj与之前的算术运算类似,读者可以自行分析.关键看它是如果实现中跳转的,如果RK==RK并且A为1 的情况下(即条件为真),则会使用pc取出下一条指令,调用dojump进行跳转,否则pc++,挂空紧接着的无条件跳转指令. dojump的实现如下:
[*]lvm.c:354
[*]#define dojump(L,pc,i) {(pc) += (i); luai_threadyield(L);}
luai_threadyield只是顺序地调用lua_unlock和lua_lock,这里为释放一次锁,使得别的线程可以得到调度.
函数调用类指令,与CALL为代表:
[*]lvm.c:582
[*] case OP_CALL: {
[*] int b = GETARG_B(i);
[*] int nresults = GETARG_C(i) - 1;
[*] if (b != 0) L->top = ra+b; /* else previous instruction set top */
[*] L->savedpc = pc;
[*] switch (luaD_precall(L, ra, nresults)) {
[*] case PCRLUA: {
[*] nexeccalls++;
[*] goto reentry; /* restart luaV_execute over new Lua function */
[*] }
[*] case PCRC: {
[*] /* it was a C function (`precall' called it); adjust results */
[*] if (nresults >= 0) L->top = L->ci->top;
[*] base = L->base;
[*] continue;
[*] }
[*] default: {
[*] return; /* yield */
[*] }
[*] }
[*] }
[*]lopcodes.c:192
[*]
[*]OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
这一条指令将在下一个介绍Lua函数调用规范的专题中详细介绍. 在这里只是简单地说明CALL指令的R表示的是即将要调用的函数,而B和C则分别表示参数个数加1,和返回值个数加1. 之所以这里需要加1,其原因是:B和C使用零来表示变长的参数和变长的返回值,而实际参数个数就向后推了一个.
指令的介绍就先到此为止了, 其它的指令的实现也比较类似.仔细阅读源码就可很容易地分析出它的意义来. 下一篇将是一个专题, 详细地介绍Lua中函数的调用是如何实现的.
小结:详解关于Lua源码分析学习教程的内容介绍完了,希望通过本文的学习能对你有所帮助!
【编辑推荐】
[*]魔兽世界超级助手Lua脚本语言
[*]LUA学习笔记:魔兽世界自定义界面插件
[*]如何在CEGUI中使用Lua脚本入门学习
[*]详解LUA特性之私有性Privacy学习笔记
[*]解析LUA下载和测试编程环境学习教程
[*]详解LUA开发工具及其环境配置
页:
[1]