「褀氏生物」完成数千万元A轮融资
06-18
作者:郑晓辉 |腾讯游戏客户端开发高级工程师在前面写道:本文所有文字都是我一一手写的,本文后面分享的Demo代码也是我一行行编码的。在我之前很多前辈都研究过Lua虚拟机,所以本文的很多想法肯定是站在这些巨人的肩膀上的。
这篇文章的标题是《Lua虚拟机深入讲解》。其实重点就在这两个字上。
毕竟作者的技术水平有限。但我听说这个名字需要有点奇怪才能让人们阅读这篇文章,因此得名。
谨以此文献给那些对Lua虚拟机感兴趣的人。希望这篇文章能够达到启发他人的效果。
Lua执行流程: Lua代码的整个流程: 如下图所示: 程序员编写Lua文件 -> 语法词法分析生成Lua字节码文件(对应Lua工具链的Luac.exe) -> 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 then) (then then (then then) 关键字) a Type_Var (这是一个变量) = Type_OpEqual (这是一个等号) c Type_Var (这是一个变量) end? ? ? ? ? Type_End (结束关键字) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?现在计算机终于明白了。原来你写的这行代码有9个单词,每个单词的意思我都明白,那么现在的问题是,计算机还看不懂这句话吗? “吃”,计算机理解“吃”是一个动词,意思是张开嘴,“饭”是一个名词,指的是米饭,但是当你把饭菜放在一起时,计算机不知道它意味着什么。
“张开嘴,把饭放进嘴里,吞进肚子里。”因为计算机只知道“张开嘴”和“米饭”这两个东西,所以计算机无法理解这两个东西之间的联系。
有人可能会说:简单:吃+其他的话。这种结构可以让计算机大致理解将后一个词所代表的东西放入口中的含义,对吗?这种情况适合“吃”这个词,但如果是这样的话,如何让计算机理解“惊喜”这个词呢?那么这就引出了下一个主题:语义解析。
关于语义解析,如果想更深入的了解,可以学习AST(抽象语法树)。然而,对于我们的例子,我们可以通过简单的方式来模拟它来理解它。
对于Lua来说,每个关键字都有自己特殊的结构。因此,Lua的关键字将成为语义分析的重点。
现在我们涉及到 if 的例子: 我们可以简单地用伪代码来表达这个解析过程: 对于 if 语句,我们可以将其抽象成这样的结构: if condition (条件表达式) then dosth (语句块) end 那么对于 if 语句语句块解析的伪代码如下: 代码语言:javascript copy ReadTokenWord(); If(tokenWord.type == Type_If) then ReadCondition() //读取条件表达式 ReadThen() //读取关键字 then ReadCodeBlock() //读取逻辑代码块 ReadEnd() //读取关键字 End End So 依次为 for计算机要理解,我们还是要把这个东西变成数据结构。因为我只是在做一个演示,所以我使用了先验知识。
也就是说,我假设我们的If语句块的逻辑结构是这样的: If 小于条件表达式 then 赋值表达式 End 那么我的Demo中转换成IfStateMent的C++数据结构大致是这样的: OK,所以现在,我们整个词典的语法分析就完成了。但是真正的Lua虚拟机无法执行我们的ifStateMent。
Lua源码中的实现类似于TokenType和结构化的if Statement whileStatement等,而Lua并没有生成完整的语法树。 Lua源码的实现中,会解析一些语句,生成临时语法树,然后翻译成指令集。
它不会等待所有句子都被解析后再进行翻译。语义解析和翻译成指令集是并行的过程。
贴出源码中语义解析的部分实现: 好了,现在我们已经把我们程序员输入的Lua代码变成了数据结构(可以被计算机读取)。下一步就是把这个数据结构变成Lua虚拟机可以理解的东西。
这个东西就是Lua指令集!至于转换过程,对于我们的例子来说,大概是这样的: 代码语言:javascript copy If a < b then a = c end 首先了解一下条件 a
那么 a
LT 后面的指令必须是 JMP 指令。即如果R[10] 同理,继续翻译a=c等。所以 If a < b then a = c end 最终被翻译成: OK 在 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,测试,测试集,调用,尾调用,返回,FORLOOP,TFORLOOP,SETLIST,关闭,关闭,VARARG。 我们发现图片上有iABC、iABx、iAsBx。这意味着有些指令格式是OPCODE、A、B、C格式,有些指令是OPCODE A、BX格式,有些指令是OPCODE A、sBX格式。 sBx和bx的区别在于bx是无符号整数,而sbx表示有符号数,即sbx可以是负数。我不打算详细讲每条指令,但我给你举个例子: 指令编码 0x 如何解析这条指令: 0x= 低六位(0~5)是操作码: 01 = 1 = LoadK指令(0~37分别对应上面我列出的38条指令,按顺序,0是Move,1是loadk,2是loadbool...37是vararg)。 LoadK指令格式为iABC(不使用C,仅使用ab)格式。那么让我们继续阅读ab。 a = 低6~13位为= 1,所以a=1 b = 低14~22位为1 = 1,所以b=1 所以0x= LOADK 1, 1 我也写了如何解析指令代码demo中,代码大致是这样的: 所以Lua文件经过Luac编译后生成的Lua字节码。 Lua字节码文件除了指令集之外还包含什么?当然它不会像我上面提到的词法和语法分析演示那么愚蠢。 那么下面我们就来说说Lua字节码文件的结构: Lua字节码文件(*.lua.bytes)包括:文件头+顶层函数: 文件头结构:顶层函数和其他普通函数结构相同:所以我们可以很容易地自己编写代码来解析它。我在后面提供的Demo源码中也实现了字节码文件的解析。 Demo中的例子涉及到Lua源代码以及字节码最终分析得到的信息是: OK,现在本文只剩下最后一件事情了:Lua虚拟机是如何执行这些指令的?大概是这样的: 代码语言:javascript Copy While (命令不为空) 执行命令 获取下一条要执行的命令 End 每个命令应该如何执行? ? ?如果你还有印象的话,我们前面语义分析后转换的指令集如下: a < bLoadK 10, 0: 将_G[ConstVar[0]]加载到10号寄存器中:R[10] = _G[" a"]LoadK 11,1: 加载 _G[ConstVar[1]] 到寄存器 11: R[11] = _G["b"]LT 10,11: 比较是否 R[10] 即如果R[10] 分配过程)。当然,指令后面的文字已经详细描述了指令的执行逻辑,呵呵。 为了真正执行起来,我们的数据结构设计需要1、寄存器:2、常量表:3、全局变量表:为了能够执行我们demo中的例子:我实现了这段代码中涉及到的所有指令代码语言: javascript 复制 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为例: 代码语言: javascript copy bool LuaVM::_Add(LuaInstruction 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指令集或者Arm??指令集来执行。 这里涉及到的寄存器EBX、ESP等都是CPU上的与非门,它们的访问速度=CPU的频率(与CPU访问内存的速度相比,简直是天上地下)。 2、指令集运行的平台:Lua指令集运行的平台就是Lua虚拟机。 C程序指令集直接在硬件支持下运行。 3、C中的数据直接对应内存地址。 Lua中的数据对应于描述该数据的数据结构。因此,分离出这样一层之后,效率就大大降低了。 4、比如Lua的Gc操作等等都是C程序不需要做的事情。 。 。 。 好吧,最后我给大家展示一下我写的这个demo的源码:这个源码是我清明节在家时瞎写的。也就是说,代码还没有耐心编译,清明节有人约我出去喝酒,导致我很长一段时间都处于“我要写完代码了”的心不在焉状态。 快点,我出去喝一杯。”所以有一些编码格式和结构设计随处可见的例子~毕竟只是一个demo。 生活中,要有佛性,顺其自然!如果你真的想了解更多关于Lua虚拟机的知识,那么我建议你有时间和耐心去阅读Lua虚拟机的源码~最后,衷心感谢所有看到最后一句话的同学。感谢您耐心阅读这篇来自技术新手的冗长废话。
版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。
标签:
相关文章
06-18
06-18
06-17
06-17
06-18
06-18
最新文章
【玩转GPU】ControlNet初学者生存指南
【实战】获取小程序中用户的城市信息(附源码)
包雪雪简单介绍Vue.js:开学
Go进阶:使用Gin框架简单实现服务端渲染
线程池介绍及实际案例分享
JMeter 注释 18 - JMeter 常用配置组件介绍
基于Sentry的大数据权限解决方案
【云+社区年度征文集】GPE监控介绍及使用