关于Lua源码分析学习教程是本文要介绍的内容,主要是了解如何在LUA中使用源码。Lua首先将源程序编译成字节码,然后交由虚拟机解释执行。对于每一个函数,Lua的编译器都会创建一个原型(prototype),它由一组使用的指令和常量组成[1]。最初的Lua虚拟机是基于堆栈的。到1993年,Lua5.0版本采用了基于寄存器的虚拟机,提高了Lua的解释效率。体系结构和指令系统与虚拟机和指令相关的主要文件有两个:lopcodes.c和lvm.c。从名字可以看出,这两个文件分别用来描述opcode(指令)和虚拟机。先看指令:Lua一共有38条指令。这些指令的名称和模式在以下两个地方描述,如下:lopcodes.c:16constchar*constluaP_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};#defineopmode(t,a,b,c,m)(((t)<<7)|((a)<<6)|((b)<<4)|((c)<<2)|(m))constlu_byteluaP_opmodes[NUM_OPCODES]={/*TABCmodeopcode*/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/***sizeandpositionofopcodearguments。*/#defineSIZE_C9#defineSIZE_B9#defineSIZE_Bx(SIZE_C+SIZE_B)#defineSIZE_A8#defineSIZE_OP6#definePOS_OP0#definePOS_A(POS_OP+SIZE_OP)#definePOS_C(POS_A+SIZE_A)#definePOS_B(POS_C+SIZE_C)#definePOS_BxPOS_C的操作模式再次指令,Lua用一个字节来表示指令的操作模式。具体含义如下:1、用最高位表示是否为测试指令。之所以特别标明这类指令,是因为Lua的指令长度是32位。很难同时表示两个操作数进行比较,同时又表示一个跳转地址。所以这条指令分为两条,第一条是测试指令,后面是无条件跳转。如果满足判断条件,则对PC(ProgramCounter,表示下一条要执行的指令)加一,跳过下一条无条件跳转指令,继续执行;否则,跳。2.第2位用于表示A操作数是否为Setting3,接下来的两位用于表示B操作数的格式,OpArgN表示未使用操作数,OpArgU表示使用操作数(立即数?),OpArgR表示操作数是寄存器或者跳转偏移量,OpArgK表示操作数是寄存器或者常量。最后给出Lua虚拟机的架构图(根据源码分析):首先,我们注意到Lua解释器仍然是一个以栈为中心的结构。在lua_State结构体中,有很多字段用来描述这个结构体。stack用于指向绝对栈底,base指向当前正执行函数的第一个参数,top指向栈顶的第一个空元素。我们可以看到这个架构中没有独立的寄存器。来自以下代码:lvm.c:343#defineRA(i)(base+GETARG_A(i))/*tobeusedafterpossiblestackreallocation*/#defineRB(i)check_exp(getBMode(GET_OPCODE(i))==OpArgR,base+GETARG_B(i))#defineRC(i)check_exp(getCMode(GET_OPCODE(i))==OpArgR,base+GETARG_C(i))#defineRKB(i)check_exp(getBMode(GET_OPCODE(i))==OpArgK,\ISK(GETARG_B(i))?k+INDEXK(GETARG_B(i)):base+GETARG_B(i))#defineRKC(i)check_exp(getCMode(GET_OPCODE(i))==OpArgK,\ISK(GETARG_C(i))?k+INDEXK(GETARG_C(i)):base+GETARG_C(i))#defineKBx(i)check_exp(getBMode(GET_OPCODE(i))==OpArgK,k+GETARG_Bx(i))当指令操作数类型为寄存器时,它的内容是基于base栈上的索引值。如图所示。寄存器实际上是基址之上的栈元素的别名;当指令操作数的类型为常量时,首先判断B操作数的最高位是否为零。如果为零,则同寄存器的处理方法。如果不为零,则在常量表中找到对应的值。我们知道Lua中函数的执行过程是这样的。首先函数入栈,然后参数依次入栈,形成图所示的栈内容。因此,R[0]至R[n]也分别代表Arg[1]至Arg[N+1]。在第一个参数下面,是当前正在执行的函数。对于Lua函数(相对于C函数),它指向一个Prototype类型的TValue。在Prototype中,字段code指向一个数组,代表组成函数的所有指令,字段k指向一个数组,代表函数使用的所有常量。最后,Lua有一个特殊的变量pc来指向下一条要执行的指令。指令解释器前面已经介绍过指令格式和架构。现在我们进入正题,看看Lua指令是如何执行的。主要函数如下:lvm.c:373voidluaV_execute(lua_State*L,intnexeccalls){LClosure*cl;数据库;T值*k;constInstruction*pc;再入:/*入口点*/lua_assert(isLua(L->ci));pc=L->savedpc;cl=&clvalue(L->ci->func)->l;基础=L->基础;k=cl->p->k;这是最初的初始化过程。其中,pc初始化为L->savedpc,base初始化为L->base,即程序从L->savedpc开始执行(下一题会介绍L->savedpc指向函数调用预处理时指向当前函数的代码),L->base指向当前函数在栈中的下一个位置。cl表示当前正在执行闭包(当前可以理解为函数),k指向当前闭包的常量表。接下来(注意,为了关注主要逻辑,我将其用于Debugger支持,断言等代码省略):/*mainloopofinterpreter*/for(;;){constInstructioni=*pc++;圣基德拉;/*省略调试器支持和协同程序支持*//*警告!!severalcallsmayreallocthestackandinvalidate`ra'*/ra=RA(i);/*省略断言*/switch(GET_OPCODE(i)){进入解释器的主循环,处理很简单,获取当前指令,自增pc,初始化ra,然后根据的操作码指令被选中。下面这段代码是什么,估计大家都能想到,一大串case来说明每条指令的执行情况。具体实现可以参考源码,这里不对每条指令展开,只说明主要的指令类型:传值指令,用MOVE表示:lvm.c:403caseOP_MOVE:{setobjs2s(L,ra,RB(i));继续;}lopcodes:154OP_MOVE,/*ABR(A):=R(B)*/lobject.h:161#definesetobj(L,obj1,obj2)\{constTValue*o2=(obj2);TValue*o1=(obj1);\o1->value=o2->value;o1->tt=o2->tt;\checkliveness(G(L),o1);}/***differenttypesofsets,accordingtodestination*//*fromstackto(same)stack*/#definesetobjs2ssetobj从注释看,这条指令使用操作数A和B作为寄存器,然后将B的值赋值给A。实现起来也简单明了,就用一句。宏展开后可以看到R[A]和R[B]的类型都是TValue,只需要传这两个字段的值即可。对于可回收对象,真正的值不会保存在栈上,所以只改变指针,而对于不可回收对象,值直接从R[B]赋值给R[A]。数值运算指令,用ADD表示:lvm.c:470caseOP_ADD:{arith_op(luai_numadd,TM_ADD);继续;}lvm.c:360#definearith_op(op,tm){\TValue*rb=RKB(i);\TValue*rc=RKC(i);\if(ttisnumber(rb)&&ttisnumber(rc)){\lua_Numbernb=nvalue(rb),nc=nvalue(rc);\setnvalue(ra,op(nb,nc));\}\else\Protect(Arith(L,ra,rb,rc,tm));\}lopcodes.c:171OP_ADD,/*ABCR(A):=RK(B)+RK(C)*/如果两个操作数都是值,则key行是:setnvalue(ra,op(nb,nc));即,将两个操作数相加后,将值赋给R[A]。值得注意的是,操作数B和C都是RK,即有可能是一个寄存器也可能是一个常量,取决于B和C的最高位是否为1,如果为1,则是常数,否则它是一个寄存器。详情请参考宏ISK的实现。如果两个操作数不是值,即调用Arith函数,它会尝试将两个操作数转换为值进行计算,如果不能转换,则使用metatable机制。该函数的实现如下:lvm.c:313staticvoidArith(lua_State*L,StkIdra,constTValue*rb,constTValue*rc,TMSop){TValuetempb,tempc;constTValue*b,*c;if((b=luaV_tonumber(rb,&tempb))!=NULL&&(c=luaV_tonumber(rc,&tempc))!=NULL){lua_Numbernb=nvalue(b),nc=nvalue(c);switch(op){caseTM_ADD:setnvalue(ra,luai_numadd(nb,nc));中断;caseTM_SUB:setnvalue(ra,luai_numsub(nb,nc));中断;caseTM_MUL:setnvalue(ra,luai_nummul(nb,nc));中断;caseTM_DIV:setnvalue(ra,luai_numdiv(nb,nc));中断;caseTM_MOD:setnvalue(ra,luai_nummod(nb,nc));中断;caseTM_POW:setnvalue(ra,luai_numpow(nb,nc));中断;caseTM_UNM:setnvalue(ra,luai_numunm(nb));中断;默认值:lua_assert(0);中断;}}elseif(!call_binTM(L,rb,rc,ra,op))luaG_arithererror(L,rb,rc);}上面的call_binTM是用来调用metatable中的metamethod的,因为meta-method在以前的Lua版本中也叫tagmethod,所以函数以TM结尾。lvm:163staticintcall_binTM(lua_State*L,constTValue*p1,constTValue*p2,StkIdres,TMSevent){constTValue*tm=luaT_gettmbyobj(L,p1,event);/*tryfirstoperand*/if(ttisnil(tm))tm=luaT_gettmbyobj(L,p2,event);/*trysecondoperand*/if(!ttisfunction(tm)))返回0;callTMres(L,res,tm,p1,p2);返回1;}在这个函数中,尝试找到两个操作数之一的元方法(第一个操作数在前),这里的事件代表具体的元方法。找到后,使用函数callTMres()调用对应的元方法。callTMres()的实现很简单,就是将元方法、第一和第二操作数压入栈中,然后调用并获取返回值。具体如下:lvm.c:82staticvoidcallTMres(lua_State*L,StkIdres,constTValue*f,constTValue*p1,constTValue*p2){ptrdiff_tresult=savestack(L,res);setobj2s(L,L->top,f);/*pushfunction*/setobj2s(L,L->top+1,p1);/*1stargument*/setobj2s(L,L->top+2,p2);/*第二参数*/luaD_checkstack(L,3);L->顶部+=3;luaD_call(L,L->top-3,1);res=restorestack(L,结果);L->顶部--;setobjs2s(L,res,L->top);}逻辑运算指令,用EQ表示:lvm.c:541caseOP_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++;继续;}lopcodes.c:185OP_EQ,/*ABCif((RK(B)==RK(C))~=A)thenpc++*/在这条指令的执行过程中,equalobj与前面的算术运算类似,读者可以自行分析。关键是看它在实现中是否跳转,如果RK[B]==RK[C]且A为1时(即条件为真),则下一条指令会被pc取出,而会调用dojump进行跳转,否则pc++会清空下一条无条件跳转指令。dojump的实现如下:lvm.c:354#definedojump(L,pc,i){(pc)+=(i);luai_threadyield(L);}luai_threadyield只是依次调用了lua_unlock和lua_lock,这里是释放一个锁,以便可以安排其他线程。函数调用类指令,用CALL表示:lvm.c:582caseOP_CALL:{intb=GETARG_B(i);intnresults=GETARG_C(i)-1;if(b!=0)L->top=ra+b;/*elsepreviousinstructionsettop*/L->savedpc=pc;switch(luaD_precall(L,ra,nresults)){casePCRLUA:{nexeccalls++;gotoreentry;/*restartluaV_executeovernewLuafunction*/}casePCRC:{/*itwasaCfunction(`precall'calledit);adjustresults*/if(nresults>=0)L->top=L->ci->top;基础=L->基础;继续;}default:{return;/*yield*/}}}lopcodes.c:192OP_CALL,/*ABCR(A),...,R(A+C-2):=R(A)(R(A+1),...,R(A+B-1))*/this指令将在下一篇介绍Lua函数调用规范的专题中详细介绍。这里简单说明一下,CALL指令的R[A]表示要调用的函数,而B和C表示参数个数加1,返回值个数加1。这里之所以需要加1,是因为B和C用0来表示变长参数和变长返回值,实际的参数个数往后推了1。命令介绍到此结束是的,其他指令的实现也是类似的。仔细阅读源码可以很容易地分析出它的含义。下一篇文章会做专题,详细介绍Lua中的函数调用是如何实现的。总结:详解Lua源码分析学习教程介绍到此结束,希望本文对您有所帮助!
