大家好,h视频网站免费源码分享相信很多的网友都不是很明白,包括hl视频也是一样,不过没有关系,接下来就来为大家分享关于h视频网站免费源码分享和hl视频的一些知识点,大家可以关注收藏,免得下次来找不到哦,下面我们开始吧!
本文从一个简单示例入手,详细讲解Lua字节码文件的存储结构及各字段含义,进而引出Lua虚拟机指令集和运行时的核心数据结构LuaState,最后解释Lua虚拟机的47条指令如何在LuaState上运作的。
为了达到较高的执行效率,lua代码并不是直接被Lua解释器解释执行,而是会先编译为字节码,然后再交给lua虚拟机去执行。lua代码称为chunk,编译成的字节码则称为二进制chunk(Binarychunk)。lua.exe、wlua.exe解释器可直接执行lua代码(解释器内部会先将其编译成字节码),也可执行使用luac.exe将lua代码预编译(Precompiled)为字节码。使用预编译的字节码并不会加快脚本执行的速度,但可以加快脚本加载的速度,并在一定程度上保护源代码。luac.exe可作为编译器,把lua代码编译成字节码,同时可作为反编译器,分析字节码的内容。
luac.exe-v//显示luac的版本号\nluac.exeHello.lua//\n在当前目录下,编译得到Hello.lua的二进制chunk文件luac.out(默认含调试符号)\n\nluac.exe-oHello.outHello1.luaHello2.lua//\n在当前目录下,编译得到Hello1.lua和Hello2.lua的二进制chunk文件Hello.out(默认含调试符号)\n\nluac.exe-s-od:\\\\Hello.outHello.lua//\n编译得到Hello.lua的二进制chunk文件d:\\\\Hello.out(去掉调试符号)\n\nluac.exe-pHello1.luaHello2.lua//\n对Hello1.lua和Hello2.lua只进行语法检测(注:只会检查语法规则,不会检查变量、函数等是否定义和实现,函数参数返回值是否合法)
lua编译器以函数为单位对源代码进行编译,每个函数会被编译成一个称之为原型(Prototype)的结构,原型主要包含6部分内容:函数基本信息(basicinfo,含参数数量、局部变量数量等信息)、字节码(bytecodes)、常量(constants)表、upvalue(闭包捕获的非局部变量)表、调试信息(debuginfo)、子函数原型列表(subfunctions)。
原型结构使用这种嵌套递归结构,来描述函数中定义的子函数:
注:lua允许开发者可将语句写到文件的全局范围中,这是因为lua在编译时会将整个文件放到一个称之为main函数中,并以它为起点进行编译。
Hello.lua源代码如下:
print(&34;)\nfunctionadd(a,b)\nreturna+b\nend
编译得到的Hello.out的二进制为:
二进制chunk(Binarychunk)的格式并没有标准化,也没有任何官方文档对其进行说明,一切以lua官方实现的源代码为准。其设计并没有考虑跨平台,对于需要超过一个字节表示的数据,必须要考虑大小端(Endianness)问题。
lua官方实现的做法比较简单:编译lua脚本时,直接按照本机的大小端方式生成二进制chunk文件,当加载二进制chunk文件时,会探测被加载文件的大小端方式,如果和本机不匹配,就拒绝加载。二进制chunk格式设计也没有考虑不同lua版本之间的兼容问题,当加载二进制chunk文件时,会检测其版本号,如果和当前lua版本不匹配,就拒绝加载。另外,二进制chunk格式设计也没有被刻意设计得很紧凑。在某些情况下,一段lua代码编译成二进制chunk后,甚至会被文本形式的源代码还要大。预编译成二进制chunk主要是为了提升加载速度,因此这也不是很大的问题。
头部字段:
嵌套的函数原型:
注1:二进制chunk中的字符串分为三种情况:
①NULL字符串用0x00表示;
②长度小于等于253(0xFD)的字符串,先用1个byte存储字符串长度+1的数值,然后是字节数组;
③长度大于等于254(0xFE)的字符串,第一个字节是0xFF,后面跟一个8字节size_t类型存储字符串长度+1的数值,然后是字节数组。
注2:常量tag对应表
查看二进制chunk中的所有函数(精简模式):
luac.exe-lHello.lua
luac.exe-lHello.out
注1:每个函数信息包括两个部分:前面两行是函数的基本信息,后面是函数的指令列表。
注2:函数的基本信息包括:函数名称、函数的起始行列号、函数包含的指令数量、函数地址。函数的参数params个数(0+表示函数为不固定参数)、寄存器slots数量、upvalue数量、局部变量locals数量、常量constants数量、子函数functions数量。
注3:指令列表里的每一条指令包含指令序号、对应代码行号、操作码和操作数。分号后为luac生成的注释,以便于我们理解指令。
注4:整个文件内容被放置到了main函数中,并以它作为嵌套起点。
查看二进制chunk中的所有函数(详细模式):
luac.exe-l-lHello.lua注:参数为2个-l
luac.exe-l-lHello.out注:详细模式下,luac会把常量表、局部变量表和upvalue表的信息也打印出来
main<Test2.lua:0,0>(6instructionsat0046e528)\n0+params,2slots,1upvalue,0locals,3constants,1function\n序号代码行指令\n1[1]GETTABUP00-1;_ENV&34;//GETTABUPABC//将upvalues表索引为B:0的upvalue(即:_ENV)中key为常量表索引为C:-1的(即print),放到寄存器索引为A:0的地方\n2[1]LOADK1-2;&34;//LOADKABx//将常量表索引为Bx:-2的hello加载到寄存器索引为A:1的地方\n3[1]CALL021;//CALLABC//调用寄存器索引为A:0的函数,参数个数为B:2减1(即1个),C:1表示无返回值\n4[5]CLOSURE00;0046e728//CLOSUREABx//将子函数原型列表索引为Bx:0的函数地址,放到寄存器索引为A:0的地方\n5[3]SETTABUP0-30;_ENV&34;//SETTABUPABC//将upvalues表索引为A:0的upvalue(即:_ENV)中key为常量表索引为B:-3(即add),设置为寄存器索引为C:0指向的值\n6[5]RETURN01;//RETURNAB//B:1表示无返回值\nconstants(3)for0046e528:\n序号常量名\n1&34;\n2&34;\n3&34;\nlocals(0)for0046e528:\nupvalues(1)for0046e528:\n序号upvalue名是否为直接外围函数的局部变量在外围函数调用帧的索引\n0_ENV10\n\nfunction<Test2.lua:3,5>(3instructionsat0046e728)\n2params,3slots,0upvalues,2locals,0constants,0functions\n序号代码行指令\n1[4]ADD201;//ADDABC//将寄存器索引为0、1的两个数相加得到的结果放到寄存器索引为2的地方\n2[4]RETURN22;//RETURNAB//B:2表示有一个返回值A:2表示返回值在寄存器索引为2的地方\n3[5]RETURN01;//RETURNAB//B:1表示无返回值\nconstants(0)for0046e728:\nlocals(2)for0046e728:\n寄存器索引起始指令序号终止指令序号-1得到实际指令序号\n0a14;a变量的指令范围为[0,3],起始为0表示为传入的参数变量\n1b14;b变量的指令范围为[0,3]\nupvalues(0)for0046e728:
luac.exe-l-//从标准设备读入脚本,输完后按回车,然后按Ctrl+Z并回车,会打印出输入内容对应的二进制chunk内容注:进入输入模式后可按Ctrl+C强制退出
luac.exe-l–//使用上次输入,打印出二进制chunk内容
luac.exe-l-l–//使用上次输入,详细模式下打印出二进制chunk内容(参数为2个-l)
StackBasedVMvsRigisterBasedVM
高级编程语言的虚拟机是利用软件技术对硬件进行的模拟和抽象。按照实现方式,可分为两类:基于栈(StackBased)和基于寄存器(RigisterBased)。Java、.NETCLR、Python、Ruby、Lua5.0之前的版本的虚拟机都是基于栈的虚拟机;从5.0版本开始,Lua的虚拟机改成了基于寄存器的虚拟机。
一个简单的加法赋值运算:a=b+c
基于栈的虚拟机,会转化成如下指令:
pushb;//将变量b的值压入stack\n\npushc;//将变量c的值压入stack\n\nadd;//将stack顶部的两个值弹出后相加,然后将结果压入stack顶\n\nmova;//将stack顶部结果放到a中
所有的指令执行,都是基于一个操作数栈的。你想要执行任何指令时,对不起,得先入栈,然后算完了再给我出栈。总的来说,就是抽象出了一个高度可移植的操作数栈,所有代码都会被编译成字节码,然后字节码就是在玩这个栈。好处是实现简单,移植性强。坏处是指令条数比较多,数据转移次数比较多,因为每一次入栈出栈都牵涉数据的转移。
基于寄存器的虚拟机,会转化成如下指令:
addabc;//将b与c对应的寄存器的值相加,将结果保存在a对应的寄存器中
没有操作数栈这一概念,但是会有许多的虚拟寄存器。这类虚拟寄存器有别于CPU的寄存器,因为CPU寄存器往往是定址的(比如DX本身就是能存东西),而寄存器式的虚拟机中的寄存器通常有两层含义:
(1)寄存器别名(比如lua里的RA、RB、RC、RBx等),它们往往只是起到一个地址映射的功能,它会根据指令中跟操作数相关的字段计算出操作数实际的内存地址,从而取出操作数进行计算;
(2)实际寄存器,有点类似操作数栈,也是一个全局的运行时栈,只不过这个栈是跟函数走的,一个函数对应一个栈帧,栈帧里每个slot就是一个寄存器,第1步中通过别名映射后的地址就是每个slot的地址。
好处是指令条数少,数据转移次数少。坏处是单挑指令长度较长。具体来看,lua里的实际寄存器数组是用TValue结构的栈来模拟的,这个栈也是lua和C进行交互的虚拟栈。
lua指令集
Lua虚拟机的指令集为定长(Fixed-width)指令集,每条指令占4个字节(32bits),其中操作码(OpCode)占6bits,操作数(Operand)使用剩余的26bits。Lua5.3版本共有47条指令,按功能可分为6大类:常量加载指令、运算符相关指令、循环和跳转指令、函数调用相关指令、表操作指令和Upvalue操作指令。
按编码模式分为4类:iABC(39)、iABx(3)、iAsBx(4)、iAx(1)
4种模式中,只有iAsBx下的sBx操作数会被解释成有符号整数,其他情况下操作数均被解释为无符号整数。操作数A主要用来表示目标寄存器索引,其他操作数按表示信息可分为4种类型:OpArgN、OpArgU、OpArgR、OpArgK:
Lua栈索引
注1:绝对索引是从1开始由栈底到栈顶依次增长的;
注2:相对索引是从-1开始由栈顶到栈底依次递减的(在luaAPI函数内部会将相对索引转换为绝对索引);
注3:上图栈的容量为7,栈顶绝对索引为5,有效索引范围为:[1,5],可接受索引范围为:[1,7];
注4:Lua虚拟机指令里寄存器索引是从0开始的,而LuaAPI里的栈索引是从1开始的,因此当需要把寄存器索引当成栈索引使用时,要进行+1。
LuaState
指令表
下面是Lua的47条指令详细说明:
B:1CA:3MOVE
把源寄存器(索引由B指定)里的值移动到目标寄存器(索引有A指定),常用于局部变量赋值和参数传递。
公式:R(A):=R(B)
Bx:2A:4LOADK
给单个寄存器(索引由A指定)设置成常量(其在常量表的索引由Bx指定),将常量表里的某个常量加载到指定寄存器。
在lua中,数值型、字符串型等局部变量赋初始值(数字和字符串会放到常量表中):
公式:R(A):=Kst(Bx)
BxA:4LOADKX
Ax:585028EXTRAARG
LOADK使用Bx(18bits,最大无符号整数为262143)表示常量表索引。当将lua作数据描述语言使用时,常量表可能会超过这个限制,为了应对这种情况,lua提供了LOADKX指令。LOADKX指令需要和EXTRAAG指令搭配使用,用后者的Ax(26bits)操作数来指定常量索引。
公式:R(A):=Kst(Ax)
指令名称类型操作码BCALOADBOOLiABC0x03OpArgUOpArgU目标寄存器idx
B:0C:1A:2LOADBOOL
给单个寄存器(索引由A指定)设置布尔值(布尔值由B指定),如果寄存器C为非0则跳过下一条指令。
公式:
R(A):=(bool)B
if(C)pc++
指令名称类型操作码BCALOADNILiABC0x04OpArgUOpArgN目标寄存器idx
B:4CA:0LOADNIL
将序号[A,A+B]连续B+1个寄存器设置成nil值,用于给连续n个寄存器放置nil值。在lua中,局部变量的默认初始值为nil,LOADNIL指令常用于给连续n个局部变量设置初始值。
公式:R(A),R(A+1),…,R(A+B):=nil
指令名称类型操作码BCAGETUPVALiABC0x05OpArgUOpArgN目标寄存器idx
B:1CA:3GETUPVAL
把当前闭包的某个Upvalue值(索引由B指定)拷贝到目标寄存器(索引由A指定)中。
公式:R(A):=Upvalue[B]
指令名称类型操作码BCAGETTABUPiABC0x06OpArgUOpArgK目标寄存器idx
B:0C:0x002A:3GETTABUP
把当前闭包的某个Upvalue值(索引由B指定)拷贝到目标寄存器(索引由A指定)中,与GETUPVAL不同的是,Upvalue从表里取值(键由C指定,为寄存器或常量表索引)。
R(A):=Upvalue[B][rk(c)]
指令名称类型操作码BCAGETTABLEiABC0x07OpArgROpArgK目标寄存器idx
B:0C:0x002A:3GETTABLE
把表中某个值拷贝到目标寄存器(索引由A指定)中,表所在寄存器索引由B指定,键由C(为寄存器或常量表索引)指定。
公式:R(A):=R[B][rk(c)]
指令名称类型操作码BCASETTABUPiABC0x08OpArgKOpArgK目标寄存器idx
B:0x002C:0x003A:0SETTABUP
设置当前闭包的某个Upvalue值(索引由A指定)为寄存器或常量表的某个值(索引由C指定),与SETUPVAL不同的是,Upvalue从表里取值(键由B指定,为寄存器或常量表索引)。
Upvalue[A][rk(b)]:=RK(C)
指令名称类型操作码BCASETUPVALiABC0x09OpArgUOpArgN目标寄存器idx
B:0CA:3SETUPVAL
设置当前闭包的某个Upvalue值(索引由B指定)为寄存器的某个值(索引由A指定)。
公式:Upvalue[B]:=R(A)
指令名称类型操作码BCASETTABLEiABC0x0AOpArgKOpArgK目标寄存器idx
B:0x002C:0x003A:1SETTABLE
给寄存器中的表(索引由A指定)的某个键进行赋值,键和值分别由B和C指定(为寄存器或常量表索引)。
公式:R(A)[RK(B)]:=RK(C)
指令名称类型操作码BCANEWTABLEiABC0x0BOpArgUOpArgU目标寄存器idx
B:0C:2A:4NEWTABLE
创建空表,并将其放入指定寄存器(索引有A指定),表的初始数组容量和哈希表容量分别有B和C指定。
公式:R(A):={}(size=B,C)
指令名称类型操作码BCASELFiABC0x0COpArgROpArgK目标寄存器idx
B:1C:0x100A:2SELF
把寄存器中对象(索引由B指定)和常量表中方法(索引由C指定)拷贝到相邻的两个目标寄存器中,起始目标寄存器的索引由A指定。
公式:
R(A+1):=R(B)
R(A):=R(B)[RK(C)]
指令名称类型操作码BCAADDiABC0x0DOpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4ADD
对两个寄存器或常量值(索引由B和C指定)进行相加,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)+RK(C)
指令名称类型操作码BCASUBiABC0x0EOpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4SUB
对两个寄存器或常量值(索引由B和C指定)进行相减,并将结果放入另一个寄存器中(索引由A指定)
公式:
R(A):=RK(B)-RK(C)
指令名称类型操作码BCAMULiABC0x0FOpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4MUL
对两个寄存器或常量值(索引由B和C指定)进行相乘,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)*RK(C)
指令名称类型操作码BCAMODiABC0x10OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4MOD
对两个寄存器或常量值(索引由B和C指定)进行求摸运算,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)%RK(C)
指令名称类型操作码BCAPOWiABC0x11OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4POW
对两个寄存器或常量值(索引由B和C指定)进行求幂运算,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)^RK(C)
指令名称类型操作码BCADIViABC0x12OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4DIV
对两个寄存器或常量值(索引由B和C指定)进行相除,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)/RK(C)
指令名称类型操作码BCAIDIViABC0x13OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4IDIV
对两个寄存器或常量值(索引由B和C指定)进行相整除,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)//RK(C)
指令名称类型操作码BCABANDiABC0x14OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4BAND
对两个寄存器或常量值(索引由B和C指定)进行求与操作,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)&RK(C)
指令名称类型操作码BCABORiABC0x15OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4BOR
对两个寄存器或常量值(索引由B和C指定)进行求或操作,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)|RK(C)
指令名称类型操作码BCABXORiABC0x16OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4BXOR
对两个寄存器或常量值(索引由B和C指定)进行求异或操作,并将结果放入另一个寄存器中(索引由A指定)
公式:R(A):=RK(B)~RK(C)
指令名称类型操作码BCASHLiABC0x17OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4SHL
索引由B指定的寄存器或常量值进行左移位操作(移动位数的索引由C指定的寄存器或常量值),并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)<<RK(C)
指令名称类型操作码BCASHRiABC0x18OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:4SHR
索引由B指定的寄存器或常量值进行右移位操作(移动位数的索引由C指定的寄存器或常量值),并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=RK(B)>>RK(C)
指令名称类型操作码BCAUNMiABC0x19OpArgROpArgN目标寄存器idx
B:1CA:3UNM
对寄存器(索引由B指定)进行取负数操作,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=-R(B)
指令名称类型操作码BCABNOTiABC0x1AOpArgROpArgN目标寄存器idx
B:1CA:3BNOT
对寄存器(索引由B指定)进行取反操作,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=~R(B)
指令名称类型操作码BCANOTiABC0x1BOpArgROpArgN目标寄存器idx
B:1CA:3NOT
对寄存器(索引由B指定)进行求非操作,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=notR(B)
指令名称类型操作码BCALENiABC0x1COpArgROpArgN目标寄存器idx
B:1CA:3LEN
对寄存器(索引由B指定)进行求长度操作,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=lengthofR(B)
指令名称类型操作码BCACONCATiABC0x1DOpArgROpArgR目标寄存器idx
B:2C:4A:1CONCAT
将连续n个寄存器(起始索引和终止索引由B和C指定)里的值进行拼接,并将结果放入另一个寄存器中(索引由A指定)。
公式:R(A):=R(B)…….R(C)
指令名称类型操作码sBxAJMPiAsBx0x1EOpArgR目标寄存器idx
sBx:-1AJMP
当sBx不为0时,进行无条件跳转,执行pc=pc+sBx(sBx为-1,表示将当前指令再执行一次注:这将是一个死循环)
sBx:0A:0x001JMP;
当sBx为0时(继续执行后面指令,不跳转),用于闭合处于开启状态的Upvalue(即:把即将销毁的局部变量的值复制出来,并更新到某个Upvalue中)。
当前闭包的某个Upvalue值的索引由A指定:
指令名称类型操作码BCAEQiABC0x1FOpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:1EQ
寄存器或常量表(索引由B指定)是否等于寄存器或常量表(索引由C指定),若结果等于操作数A,则跳过下一条指令。
公式:if((RK(B)==RK(C))pc++
指令名称类型操作码BCALTiABC0x20OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:1LT
寄存器或常量表(索引由B指定)是否小于寄存器或常量表(索引由C指定),若结果等于操作数A,则跳过下一条指令。
公式:if((RK(B)<RK(C))pc++
指令名称类型操作码BCALEiABC0x21OpArgKOpArgK目标寄存器idx
B:0x001C:0x100A:1LE
寄存器或常量表(索引由B指定)是否小于等于寄存器或常量表(索引由C指定),若结果等于操作数A,则跳过下一条指令。
公式:if((RK(B)<=RK(C))pc++
指令名称类型操作码BCATESTiABC0x22OpArgNOpArgU目标寄存器idx
BC:0A:1TEST
判断寄存器(索引由A指定)中的值转换为bool值后,是否和操作数C表示的bool值一致,若结果不一致,则跳过下一条指令。
公式:
ifnot(R(A)<=>C)pc++
注:<=>表示按bool值比较
指令名称类型操作码BCATESTSETiABC0x23OpArgROpArgU目标寄存器idx
B:3C:0A:1TESTSET
判断寄存器(索引由B指定)中的值转换为bool值后,是否和操作数C表示的bool值一致,若结果一致,将寄存器(索引由B指定)中的值复制到寄存器中(索引由A指定),否则跳过下一条指令。
公式:
if(R(B)\\<=\\>C)\nR(A):=R(B)\nelse\npc++
注:<=>表示按bool值比较
指令名称类型操作码BCACALLiABC0x24OpArgUOpArgU目标寄存器idx
B:5C:4A:0CALL
被调用函数位于寄存器中(索引由A指定),传递给被调用函数的参数值也在寄存器中,紧挨着被调用函数,参数个数为操作数B指定。
①B==0,接受其他函数全部返回来的参数
②B>0,参数个数为B-1
函数调用结束后,原先存放函数和参数值的寄存器会被返回值占据,具体多少个返回值由操作数C指定。
①C==0,将返回值全部返回给接收者
②C==1,无返回值
③C>1,返回值的数量为C-1
公式:R(A),…,
指令名称类型操作码BCATAILCALLiABC0x25OpArgUOpArgU目标寄存器idx
函数调用一般通过调用栈来实现。用这种方法,每调用一个函数都会产生一个调用帧。
如果调用层次太深(如递归),容易导致栈溢出。尾递归优化则可以让我们发挥递归函数调用威力的同时,避免调用栈溢出。利用这种优化,被调函数可以重用主调函数的调用帧,因此可有效缓解调用栈溢出症状。不过该优化只适合某些特定情况。
如:returnf(args)会被编译器优化成TAILCALL指令,公式:returnR(A)(R(A+1),…,R(A+B-1))
指令名称类型操作码BCARETURNiABC0x26OpArgUOpArgN目标寄存器idx
B:4CA:2RETURN
把存放在连续多个寄存器里的值返回给父函数,其中第一个寄存器的索引由操作数A指定,寄存器数量由操作数B指定,操作数C没有使用,需要将返回值推入栈顶:
①B==1,不需要返回任何值
②B>1,需要返回B-1个值;这些值已经在寄存器中了,只用再将它们复制到栈顶即可
③B==0,一部分返回值已经在栈顶了,只需将另一部分也推入栈顶即可
公式:returnR(A),…,R(A+B-2)
指令名称类型操作码sBxAFORLOOPiAsBx0x27OpArgR目标寄存器idx
数值for循环:用于按一定步长遍历某个范围内的数值如:fori=1,100,2dof()end//初始值为1,步长为2,上限为100
该指令先给i加上步长,然后判断i是否在范围之内。若已经超出范围,则循环结束;若为超出范围,则将数值拷贝给用户定义的局部变量,然后跳转到循环体内部开始执行具体的代码块。
公式:
R(A)+=R(A+2)\nifR(A)<?=R(A+1)\npc+=sBx\nR(A+3)=R(A)
注:当步长为正数时<?=为<=
当步长为负数时<?=为>=
指令名称类型操作码sBxAFORPREPiAsBx0x28OpArgR目标寄存器idx
数值for循环:用于按一定步长遍历某个范围内的数值如:fori=1,100,2dof()end//初始值为1,步长为2,上限为100。
该指令的目的是在循环之前预先将i减去步长(得到-1),然后跳转到FORLOOP指令正式开始循环:
公式:
R(A)-=R(A+2)
pc+=sBx
指令名称类型操作码BCATFORCALLiABC0x29OpArgNOpArgU目标寄存器idx
通用for循环:fork,vinpairs(t)doprint(k,v)end
编译器使用的第一个特殊变量(generator):f存放的是迭代器,其他两个特殊变量(state):s、(control):var来调用迭代器,把结果保存在用户定义的变量k、v中。
公式:R(A+3),…,R(A+2+C):=R(A)(R(A+1),R(A+2))
指令名称类型操作码sBxATFORLOOPiAsBx0x2AOpArgR目标寄存器idx
通用for循环:fork,vinpairs(t)doprint(k,v)end
若迭代器返回的第一个值(变量k)不是nil,则把该值拷贝到(control):var,然后跳转到循环体;若为nil,则循环结束。
公式:
ifR(A+1)~=nil
R(A)=R(A+1)
pc+=sBx
指令名称类型操作码BCASETLISTiABC0x2BOpArgUOpArgU目标寄存器idx
SETTABLE是通用指令,每次只处理一个键值对,具体操作交给表去处理,并不关心实际写入的是表的hash部分还是数组部分。SETLIST则是专门给数组准备的,用于按索引批量设置数组元素。其中数组位于寄存器中,索引由操作数A指定;需要写入数组的一系列值也在寄存器中,紧挨着数组,数量由操作数B指定;数组起始索引则由操作数C指定。
因为C操作数只有9bits,所以直接用它表示数组索引显然不够用。这里解决办法是让C操作数保存批次数,然后用批次数乘上批大小(FPF,默认为50)就可以算出数组的起始索引。因此,C操作数能表示的最大索引为25600(50*512),当数组长度大于25600时,SETLIST指令后会跟一条EXTRAARG指令,用其Ax操作数来保存批次数。
综上,C>0,表示的是批次数+1,否则,真正批次数存放在后续的EXTRAARG指令中。
操作数B为0时,当表构造器的最后一个元素是函数调用或者vararg表达式时,Lua会把它们产生的所有值都收集起来供SETLIST使用。
公式:
R(A)[(C-1)*FPF+i]:=R(A+i)
1<=i<=B
指令名称类型操作码BxACLOSUREiABx0x2COpArgU目标寄存器idx
把当前Lua函数的子函数原型实例化为闭包,放入由操作数A指定的寄存器中子函数原型来自于当前函数原型的子函数原型表,索引由操作数Bx指定。
下图为将prototypes表中索引为1的g子函数,放入索引为4的寄存器中:
公式:R(A):=closure(KPROTO[Bx])
指令名称类型操作码BCAVARARGiABC0x2DOpArgUOpArgN目标寄存器idx
把传递给当前函数的变长参数加载到连续多个寄存器中。
其中第一个寄存器的索引由操作数A指定,寄存器数量由操作数B指定,操作数C没有使用,操作数B若大于1,表示把B-1个vararg参数复制到寄存器中,否则只能等于0。
公式:R(A),R(A+1),…R(A+B-2)=vararg
指令名称类型操作码AxEXTRAARGiAx0x2EOpArgU
Ax:67108864EXTRAARG
Ax有26bits,用来指定常量索引,可存放最大无符号整数为67108864,可满足大部分情况的需要了。
参考
《自己动手实现Lua》源代码Lua设计与实现–虚拟机篇Lua5.3BytecodeReferenceLua源码解析
作者:nicochen,腾讯IEG游戏开发工程师
如果你还想了解更多这方面的信息,记得收藏关注本站。