欢迎你们前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~程序员
本文由 鹅厂优文发表于 云+社区专栏
做者:郑小辉 | 腾讯 游戏客户端开发高级工程师数据结构
写在前面:本文全部的文字都是我手工一个一个敲的,以及本文后面分享的Demo代码都是我一行一行码的,在我以前已经有很是多的前辈研究过Lua虚拟机了,因此本文不少思想必然是踏在这些巨人的肩膀上的。机器学习
本文标题是”深刻浅出Lua虚拟机”,其实重点在浅出这两字上。毕竟做者的技术水平有限。可是据说名字要起的屌一点文章才有人看,故而得名。函数
谨以此文奉献给那些对Lua虚拟机有兴趣的人。但愿本文能达到一个抛砖引玉的效果。工具
Lua代码的整个流程:性能
以下图所示:程序员编码lua文件->语法词法分析生成Lua的字节码文件(对应Lua工具链的Luac.exe)->Lua虚拟机解析字节码,并执行其中的指令集->输出结果。学习
蓝色和绿色的部分是本文所试图去讲的内容。ui
我不许备讲Lua的全部词法分析过程,毕竟若是浪费太多时间来写这个的话一会策划同窗要提刀来问我需求的开发进度如何了,因此长话短说,我就根据本身对Lua的理解,以某一个具体的例子来作分析:编码
Lua代码块:lua
If a < b then a = c end
这句话我们程序员能看懂,但是计算机就跟某些男程序员家里负责貌美如花的老婆同样,只知道这是一串用英文字符拼出来的一行没有任何意义的字符串而已。
为了让计算机可以读懂这句话,那么咱们要作的第一件事情就是分词:既然你看不懂。我就先把一句话拆成一个一个单词,并且我告诉你每一个单词的含义是什么。
分词的结果大概长下面这样:
分词结果 类型(意义)
if Type_If (if 关键字)
a Type_Var (这是一个变量)
< Type_OpLess(这是一个小于号)
b Type_Var(这是一个变量)
then Type_Then(Then关键字)
a Type_Var (这是一个变量)
= Type_OpEqual(这是一个等号)
c Type_Var(这是一个变量)
end Type_End(End关键字)
好了。如今计算机终于明白了。原来你写的这行代码里面有9个字,并且每一个字的意思我都懂了。因此如今问题是,计算机理解了这句话了吗?
计算机依然不理解。就好像“吃饭”这句话,计算机理解了 “吃”是动词,张开嘴巴的意思。“饭”是名词,指的米饭的意思。可是你把吃饭放在一块儿,计算机并不知道这是“张开嘴巴,把饭放进嘴里,而且咽到胃里”的意思。由于计算机只知道“张开嘴巴”和“米饭”两件事,这两件事有什么联系,计算机并不能理解。有人会说了:简单:吃+其余字 这种结构就让计算机笼统的理解为把后一个词表明的东西放进嘴巴里的意思就行了啊?这种状况适合”吃饭”这个词,可是若是这样你让计算机怎么理解“吃惊”这个词呢?因此这里引出下一个话题:语义解析。
关于语义解析这块,若是你们想要了解的更深刻,能够去了解一下AST(抽象语法树)。然而对于咱们这个例子,咱们用简单的方式模拟着去理解就行了。
对于Lua而言,每个关键字都有本身特别的结构。因此Lua的关键字将成为语义解析的重点。咱们如今涉及到的if这个例子:咱们能够简单的用伪代码表述这个解析过程:
对于if语句咱们能够抽象成这种结构:
If condition(条件表达式) then dosth(语句块) end
因此对if语句块进行解析的伪代码以下:
ReadTokenWord(); If(tokenWord.type == Type_If) then ReadCondition() //读取条件表达式 ReadThen() //读取关键字then ReadCodeBlock() //读取逻辑代码块 ReadEnd() //读取关键字End End
因此为了让计算机理解,咱们仍是得把这个东西变成数据结构。
由于我只是作一个Demo而已,因此我用了先验知识。也就是我假定咱们的If语句块逻辑结构是这样的:
If 小于条件表达式 then 赋值表达式 End
因此在个人Demo里转成C++数据结构就是IfStateMent大概是这样:
OK,因此如今,咱们整个词法语法分析都作完了。可是真正的Lua虚拟机并不能执行咱们的ifStateMent这种东西。Lua源码里的实现也是相似这种TokenType 和 结构化的 if Statement whileStatement等等,而且Lua没有生成完整的语法树。Lua源码的实现里面,它是解析一些语句,生成临时的语法树,而后翻译成指令集的。并不会等全部的语句都解析完了再翻译的。语义解析和翻译成指令集是并行的一个过程。贴一个源码里面关于语义解析的部分实现:
OK,如今我们已经把咱们程序员输入的Lua代码变成了一个数据结构(计算机能读懂)。下一步咱们要把这个数据结构再变成Lua虚拟机能认识的东西,这个东西就是 Lua 指令集!
至于转换的过程,对于咱们这个例子,大概是这样的:
If a < b then a = c end
先理解条件 a<b:一种基于寄存器的指令设计大概是这样的:
a,b均为变量。假定咱们的可用的寄存器索引值从10(0-9号寄存器都已经被占用了)开始:又假定咱们有一个常量索引表:0号常量:字符’a’,1号常量:字符串’b’。那么a<b能够被翻译为这样:
同理,继续进行a=c的翻译等等。
因此If a < b then a = c end在我写的demo里面最后被翻译成了:
OK,咱们如今大概明白了从Lua代码怎么变成指令集的这件事了。
如今咱们来具体看一下Lua5.1的指令集:
Lua的指令集是定长的,每一条指令都是32位,其中大概长这样:
每一条指令的低六位 都是指令的指令码,好比 0表明MOVE,12表明Add。Lua总共有37条指令,分别是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,
TFORLOOP,SETLIST,CLOSE,CLOSURE,VARARG.
咱们发现图上还有iABC,iABx,iAsBx。这个意思是有的指令格式是 OPCODE,A,B,C的格式,有的指令是OPCODE A,BX格式,有的是OPCODE A,sBX格式。sBx和bx的区别是bx是一个无符号整数,而sbx表示的是一个有符号的数,也就是sbx能够是负数。
我不打算详细的讲每一条指令,我仍是举个例子:
指令编码 0x 00004041 这条指令怎么解析:
0x4041 = 0000 0000 0000 0000 0100 0000 0100 0001
低六位(0~5)是opcode:000001 = 1 = LoadK指令(0~37分别对应了我上面列的38条指令,按顺序来的,0是Move,1是loadk,2是loadbool.....37是vararg)。LoadK指令格式是iABC(C没用上,仅ab有用)格式。因此咱们再继续读ab。
a = 低6~13位 为 00000001 = 1因此a=1
b = 低14~22位 为000000001 = 1因此b=1
因此0x4041 = LOADK 1, 1
指令码如何解析我也在demo里面写了,代码大概是这样:
那么Lua文件通过Luac的编译后生成的Lua字节码,Lua字节码文件里面除了包含指令集以外又有哪些东西呢?固然不会像我上面的那个词法语法解析那个demo那么弱智拉。因此下面咱们就讲一下Lua字节码文件的结构:
Lua字节码文件(*.lua.bytes)包含了:文件头+顶层函数:
文件头结构:
顶层函数和其余普通函数都拥有一样的结构:
因此咱们是能够轻松本身写代码去解析的。后文提供的Demo源码里面我也已经实现了字节码文件的解析。
Demo中的例子是涉及到的Lua源代码以及最终解析字节码获得的信息分别是:
OK,本文如今就剩最后一点点东西了:Lua虚拟机是怎么执行这些指令的呢?
大概是这样的:
While(指令不为空) 执行指令 取下一条要执行的指令 End
每一条指令应该怎么执行呢???若是你们还有印象的话,我们前文语义解析完以后转指令集是这样的:
a < b
那固然是指令后面的文字就已经详细的描述了指令的执行逻辑拉,嘿嘿。
为了真正的执行起来,因此咱们在数据结构上设计须要 1,寄存器:2,常量表:3,全局变量表:
为了能执行咱们demo里面的例子:
我实现了这段代码涉及到的全部指令
insExecute[(int)OP_LOADK] = &LuaVM::LoadK; insExecute[(int)OP_SETGLOBAL] = &LuaVM::SetGlobal; insExecute[(int)OP_GETGLOBAL] = &LuaVM::GetGlobal; insExecute[(int)OP_ADD] = &LuaVM::_Add; insExecute[(int)OP_SUB] = &LuaVM::_Sub; insExecute[(int)OP_MUL] = &LuaVM::_Mul; insExecute[(int)OP_DIV] = &LuaVM::_Div; insExecute[(int)OP_CALL] = &LuaVM::_Call; insExecute[(int)OP_MOD] = &LuaVM::_Mod; insExecute[(int)OP_LT] = &LuaVM::_LT; insExecute[(int)OP_JMP] = &LuaVM::_JMP; insExecute[(int)OP_RETURN] = &LuaVM::_Return;
以Add为例:
bool LuaVM::_Add(LuaInstrunction ins) { //R(A):=RK(B)+RK(C) ::: //Todo:必要的参数合法性检查:若是有问题则抛异常 // 将ins.bValue表明的数据和ins.cValue表明的数据相加的结果赋值给索引值为ins.aValue的寄存器 luaRegisters[ins.aValue].SetValue(0, GetBK(ins.bValue) + GetBK(ins.cValue)); return true; }
下面是程序的运行效果截图:
看完整个过程,其实能够思考这个问题:为何Lua执行效率会远远低于C程序?
我的愚见:
1,真假寄存器:Lua指令集涉及到的寄存器是模拟的寄存器,其实质仍是内存上的一个数据。访问速度取决于CPU对内存的访问速度。而C程序最后能够用win32指令集or Arm指令集来执行。这里面涉及到的寄存器EBX,ESP等都是CPU上面的与非门,其访问速度=CPU的频率(和cpu访问内存的速度对比简直一个天上一个地上)。
2,指令集运行的平台:Lua指令集运行的平台是Lua虚拟机。而C程序指令集运行的直接是硬件支持的。
3,C里面的数据直接对应的就是内存地址。而Lua里面的数据对应的是一个描述这个数据的数据结构。因此隔了这么一层,效率也大打折扣了。
4,好比Lua的Gc操做等等这些东西都是C程序不须要去作的。。。。
OK,最后献上我写的这个demo的源代码:这份源代码是我在清明节在家的时候瞎写的。也就是说代码并无通过耐心的整理,并且清明节有人找我出去喝酒,致使我有很长一段时间都处于“我艹快点码完我要出去喝了”这种心不在焉的状态,因此有些编码格式和结构设计都到处能看到随性的例子~毕竟只是一个demo嘛。人生在世,要有佛性,随缘就好!若是各位真的想进一步理解关于Lua虚拟机的东西,那么我推荐诸位有空耐着性子去读一读Lua虚拟机的源代码~
最后,诚挚感谢全部看到了最后这句话的同窗。谢谢大家耐着性子看完了一个技术菜鸡的长篇废话。
问答
Lua支持Unicode吗?
相关阅读
Lua 性能剖析
使用lua小技巧
Lua 游戏开发学习
【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识
此文已由做者受权腾讯云+社区发布,更多原文请点击
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
海量技术实践经验,尽在云加社区!