lua 去除小数点有效数字后面的0_万字详文:深入理解 Lua 虚拟机

本文从一个简单示例入手,详细讲解 Lua 字节码文件的存储结构及各字段含义,进而引出 Lua 虚拟机指令集和运行时的核心数据结构 Lua State,最后解释 Lua 虚拟机的 47 条指令如何在 Lua State 上运作的。

为了达到较高的执行效率,lua 代码并不是直接被 Lua 解释器解释执行,而是会先编译为字节码,然后再交给 lua 虚拟机去执行。lua 代码称为 chunk,编译成的字节码则称为二进制 chunk(Binary chunk)。lua.exe、wlua.exe 解释器可直接执行 lua 代码(解释器内部会先将其编译成字节码),也可执行使用 luac.exe 将 lua 代码预编译(Precompiled)为字节码。使用预编译的字节码并不会加快脚本执行的速度,但可以加快脚本加载的速度,并在一定程度上保护源代码。luac.exe 可作为编译器,把 lua 代码编译成字节码,同时可作为反编译器,分析字节码的内容。

luac.exe -v  // 显示luac的版本号luac.exe Hello.lua  //在当前目录下,编译得到Hello.lua的二进制chunk文件luac.out(默认含调试符号)luac.exe -o Hello.out Hello1.lua Hello2.lua //在当前目录下,编译得到Hello1.lua和Hello2.lua的二进制chunk文件Hello.out(默认含调试符号)luac.exe -s -o d:Hello.out Hello.lua  //编译得到Hello.lua的二进制chunk文件d:Hello.out(去掉调试符号)luac.exe -p Hello1.lua Hello2.lua  //对Hello1.lua和Hello2.lua只进行语法检测(注:只会检查语法规则,不会检查变量、函数等是否定义和实现,函数参数返回值是否合法)

lua 编译器以函数为单位对源代码进行编译,每个函数会被编译成一个称之为原型(Prototype)的结构,原型主要包含 6 部分内容:函数基本信息(basic info,含参数数量、局部变量数量等信息)、字节码(bytecodes)、常量(constants)表、upvalue(闭包捕获的非局部变量)表、调试信息(debug info)、子函数原型列表(sub functions)。

原型结构使用这种嵌套递归结构,来描述函数中定义的子函数:

c73df5fdbbb795fcc4c333092bcf1b7f.png

注:lua 允许开发者可将语句写到文件的全局范围中,这是因为 lua 在编译时会将整个文件放到一个称之为 main 函数中,并以它为起点进行编译。

Hello.lua 源代码如下:

print ("hello")function add(a, b)    return a+bend

编译得到的 Hello.out 的二进制为:

8e5185d309270004b43a70d4ad6293f6.png

二进制 chunk(Binary chunk)的格式并没有标准化,也没有任何官方文档对其进行说明,一切以 lua 官方实现的源代码为准。其设计并没有考虑跨平台,对于需要超过一个字节表示的数据,必须要考虑大小端(Endianness)问题。

lua 官方实现的做法比较简单:编译 lua 脚本时,直接按照本机的大小端方式生成二进制 chunk 文件,当加载二进制 chunk 文件时,会探测被加载文件的大小端方式,如果和本机不匹配,就拒绝加载。二进制 chunk 格式设计也没有考虑不同 lua 版本之间的兼容问题,当加载二进制 chunk 文件时,会检测其版本号,如果和当前 lua 版本不匹配,就拒绝加载。另外,二进制 chunk 格式设计也没有被刻意设计得很紧凑。在某些情况下,一段 lua 代码编译成二进制 chunk 后,甚至会被文本形式的源代码还要大。预编译成二进制 chunk 主要是为了提升加载速度,因此这也不是很大的问题。

头部字段

123fc72dc9af7515a87249a61c3a58f8.png

嵌套的函数原型

f67eb5ece7bee0cb03f2921967e0cc8e.png

注 1:二进制 chunk 中的字符串分为三种情况:

①NULL 字符串用 0x00 表示;

② 长度小于等于 253(0xFD)的字符串,先用 1 个 byte 存储字符串长度+1 的数值,然后是字节数组;

③ 长度大于等于 254(0xFE)的字符串,第一个字节是 0xFF,后面跟一个 8 字节 size_t 类型存储字符串长度+1 的数值,然后是字节数组。

注 2:常量 tag 对应表

893f6c8f544de23d44f8df3cd220b800.png

查看二进制 chunk 中的所有函数(精简模式):

luac.exe -l Hello.lua

luac.exe -l Hello.out

9d89495e96c5a55151c1e66dfbbb1250.png

注 1:每个函数信息包括两个部分:前面两行是函数的基本信息,后面是函数的指令列表。

注 2:函数的基本信息包括:函数名称、函数的起始行列号、函数包含的指令数量、函数地址。函数的参数 params 个数(0+表示函数为不固定参数)、寄存器 slots 数量、upvalue 数量、局部变量 locals 数量、常量 constants 数量、子函数 functions 数量。

注 3:指令列表里的每一条指令包含指令序号、对应代码行号、操作码和操作数。分号后为 luac 生成的注释,以便于我们理解指令。

注 4:整个文件内容被放置到了 main 函数中,并以它作为嵌套起点。

查看二进制 chunk 中的所有函数(详细模式):

luac.exe -l -l Hello.lua 注:参数为 2 个-l

luac.exe -l -l Hello.out 注:详细模式下,luac 会把常量表、局部变量表和 upvalue 表的信息也打印出来

main <0> (6 instructions at 0046e528)0+ params, 2 slots, 1 upvalue, 0 locals, 3 constants, 1 function        序号    代码行    指令        1       [1]     GETTABUP        0 0 -1  ; _ENV "print"   //GETTABUP A B C  //将upvalues表索引为B:0的upvalue(即:_ENV)中key为常量表索引为C:-1的(即print),放到寄存器索引为A:0的地方        2       [1]     LOADK           1 -2    ; "hello"  //LOADK A Bx  //将常量表索引为Bx:-2的hello加载到寄存器索引为A:1的地方        3       [1]     CALL            0 2 1    ; //CALL A B C  //调用寄存器索引为A:0的函数,参数个数为B:2减1(即1个),C:1表示无返回值        4       [5]     CLOSURE         0 0     ; 0046e728      //CLOSURE A Bx  //将子函数原型列表索引为Bx:0的函数地址,放到寄存器索引为A:0的地方        5       [3]     SETTABUP        0 -3 0  ; _ENV "add"   //SETTABUP A B C  //将upvalues表索引为A:0的upvalue(即:_ENV)中key为常量表索引为B:-3(即add),设置为寄存器索引为C:0指向的值        6       [5]     RETURN          0 1        ; //RETURN A B   //B:1表示无返回值constants (3) for 0046e528:        序号    常量名        1       "print"        2       "hello"        3       "add"locals (0) for 0046e528:upvalues (1) for 0046e528:        序号    upvalue名    是否为直接外围函数的局部变量    在外围函数调用帧的索引        0       _ENV        1                               0function <3> (3 instructions at 0046e728)2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions        序号    代码行    指令        1       [4]     ADD             2 0 1    ; //ADD A B C  //将寄存器索引为0、1的两个数相加得到的结果放到寄存器索引为2的地方        2       [4]     RETURN          2 2        ; //RETURN A B //B:2表示有一个返回值  A:2表示返回值在寄存器索引为2的地方        3       [5]     RETURN          0 1        ; //RETURN A B //B:1表示无返回值constants (0) for 0046e728:locals (2) for 0046e728:    寄存器索引    起始指令序号  终止指令序号  -1得到实际指令序号        0       a       1       4        ; a变量的指令范围为[0, 3],起始为0表示为传入的参数变量        1       b       1       4        ; b变量的指令范围为[0, 3]upvalues (0) for 0046e728:3>0>

luac.exe -l - // 从标准设备读入脚本,输完后按回车,然后按 Ctrl+Z 并回车,会打印出输入内容对应的二进制 chunk 内容 注:进入输入模式后可按 Ctrl+C 强制退出

luac.exe -l -- // 使用上次输入,打印出二进制 chunk 内容

luac.exe -l -l -- // 使用上次输入,详细模式下打印出二进制 chunk 内容(参数为 2 个-l)

Stack Based VM vs Rigister Based VM

高级编程语言的虚拟机是利用软件技术对硬件进行的模拟和抽象。按照实现方式,可分为两类:基于栈(Stack Based)和基于寄存器(Rigister Based)。Java、.NET CLR、Python、Ruby、Lua5.0 之前的版本的虚拟机都是基于栈的虚拟机;从 5.0 版本开始,Lua 的虚拟机改成了基于寄存器的虚拟机。

一个简单的加法赋值运算:a=b+c

基于栈的虚拟机,会转化成如下指令:

push b; // 将变量b的值压入stackpush c; // 将变量c的值压入stackadd; // 将stack顶部的两个值弹出后相加,然后将结果压入stack顶mov a; // 将stack顶部结果放到a中

所有的指令执行,都是基于一个操作数栈的。你想要执行任何指令时,对不起,得先入栈,然后算完了再给我出栈。总的来说,就是抽象出了一个高度可移植的操作数栈,所有代码都会被编译成字节码,然后字节码就是在玩这个栈。好处是实现简单,移植性强。坏处是指令条数比较多,数据转移次数比较多,因为每一次入栈出栈都牵涉数据的转移。

基于寄存器的虚拟机,会转化成如下指令:

add a b c; // 将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)

64dd39668dcdb61321332699402f8793.png

4 种模式中,只有 iAsBx 下的 sBx 操作数会被解释成有符号整数,其他情况下操作数均被解释为无符号整数。操作数 A 主要用来表示目标寄存器索引,其他操作数按表示信息可分为 4 种类型:OpArgN、OpArgU、OpArgR、OpArgK:

e2c39622486b09bdd9e3c5a73df41bb4.png

Lua 栈索引

1c4201a4281459e5c4ac245722f2dfaa.png

注 1:绝对索引是从 1 开始由栈底到栈顶依次增长的;

注 2:相对索引是从-1 开始由栈顶到栈底依次递减的(在 lua API 函数内部会将相对索引转换为绝对索引);

注 3:上图栈的容量为 7,栈顶绝对索引为 5,有效索引范围为:[1,5],可接受索引范围为:[1, 7];

注 4:Lua 虚拟机指令里寄存器索引是从 0 开始的,而 Lua API 里的栈索引是从 1 开始的,因此当需要把寄存器索引当成栈索引使用时,要进行+1。

Lua State

a494e9f632a0df9493a4b0dc3ee17674.png

指令表

下面是 Lua 的 47 条指令详细说明:

884b3455e47f2a61d59ad26090802227.png

B:1 C A:3 MOVE

把源寄存器(索引由 B 指定)里的值移动到目标寄存器(索引有 A 指定),常用于局部变量赋值和参数传递。

59ecd48ae7895d9eba3b52722187086b.png

公式:R(A) := R(B)

0ceab2ce8d76ae88364e54991033121a.png

Bx:2 A:4 LOADK

给单个寄存器(索引由 A 指定)设置成常量(其在常量表的索引由 Bx 指定),将常量表里的某个常量加载到指定寄存器。

在 lua 中,数值型、字符串型等局部变量赋初始值 (数字和字符串会放到常量表中):

de4a9a962b95f93c856f8f171b665d92.png

公式:R(A) := Kst(Bx)

c60544ae84483581e4c35fb374a09c0c.png

Bx A:4 LOADKX

Ax:585028 EXTRAARG

LOADK 使用 Bx(18bits,最大无符号整数为 262143)表示常量表索引。当将 lua 作数据描述语言使用时,常量表可能会超过这个限制,为了应对这种情况,lua 提供了 LOADKX 指令。LOADKX 指令需要和 EXTRAAG 指令搭配使用,用后者的 Ax(26bits)操作数来指定常量索引。

公式:R(A) := Kst(Ax)

指令名称类型操作码BCALOADBOOLiABC0x03OpArgUOpArgU目标寄存器 idx

B:0 C:1 A:2 LOADBOOL

给单个寄存器(索引由 A 指定)设置布尔值(布尔值由 B 指定),如果寄存器 C 为非 0 则跳过下一条指令。

f85017d6cbd53cea6284c3bacdb3da00.png

公式:

R(A) := (bool)B

if(C) pc++

指令名称类型操作码BCALOADNILiABC0x04OpArgUOpArgN目标寄存器 idx

B:4 C A:0 LOADNIL

将序号[A,A+B]连续 B+1 个寄存器设置成 nil 值,用于给连续 n 个寄存器放置 nil 值。在 lua 中,局部变量的默认初始值为 nil,LOADNIL 指令常用于给连续 n 个局部变量设置初始值。

2ff77459a8ba2d3c7dd2eb50fd401e3c.png

公式:R(A), R(A+1), ... ,R(A+B) := nil

指令名称类型操作码BCAGETUPVALiABC0x05OpArgUOpArgN目标寄存器 idx

B:1 C A:3 GETUPVAL

把当前闭包的某个 Upvalue 值(索引由 B 指定)拷贝到目标寄存器(索引由 A 指定)中 。

495af0b050ed551592eab6e0bee63179.png

公式:R(A) := Upvalue[B]

指令名称类型操作码BCAGETTABUPiABC0x06OpArgUOpArgK目标寄存器 idx

B:0 C:0x002 A:3 GETTABUP

把当前闭包的某个 Upvalue 值(索引由 B 指定)拷贝到目标寄存器(索引由 A 指定)中,与 GETUPVAL 不同的是,Upvalue 从表里取值(键由 C 指定,为寄存器或常量表索引)。

1512154ce88d71ae719684ef1300af21.png

R(A) := Upvalue[B][rk(c)]

指令名称类型操作码BCAGETTABLEiABC0x07OpArgROpArgK目标寄存器 idx

B:0 C:0x002 A:3 GETTABLE

把表中某个值拷贝到目标寄存器(索引由 A 指定)中,表所在寄存器索引由 B 指定,键由 C(为寄存器或常量表索引)指定。

3cdceef5826e543d4de0ddeb6b15a190.png

公式:R(A) := R[B][rk(c)]

指令名称类型操作码BCASETTABUPiABC0x08OpArgKOpArgK目标寄存器 idx

B:0x002 C:0x003 A:0 SETTABUP

设置当前闭包的某个 Upvalue 值(索引由 A 指定)为寄存器或常量表的某个值(索引由 C 指定),与 SETUPVAL 不同的是,Upvalue 从表里取值(键由 B 指定,为寄存器或常量表索引)。

fb1197a03c808612d38a762c4ac4fb22.png

Upvalue[A][rk(b)] := RK(C)

指令名称类型操作码BCASETUPVALiABC0x09OpArgUOpArgN目标寄存器 idx

B:0 C A:3 SETUPVAL

设置当前闭包的某个 Upvalue 值(索引由 B 指定)为寄存器的某个值(索引由 A 指定)。

770eff45350944e459f43f90f9924b79.png

公式:Upvalue[B] := R(A)

指令名称类型操作码BCASETTABLEiABC0x0AOpArgKOpArgK目标寄存器 idx

B:0x002 C:0x003 A:1 SETTABLE

给寄存器中的表(索引由 A 指定)的某个键进行赋值,键和值分别由 B 和 C 指定(为寄存器或常量表索引)。

3ff4a5f6210ab4d0478578db42d2f2b2.png

公式:R(A)[RK(B)] := RK(C)

指令名称类型操作码BCANEWTABLEiABC0x0BOpArgUOpArgU目标寄存器 idx

B:0 C:2 A:4 NEWTABLE

创建空表,并将其放入指定寄存器(索引有 A 指定),表的初始数组容量和哈希表容量分别有 B 和 C 指定。

975a404fa5cfd913511b1e6bef7bc0b3.png

公式:R(A) := {} (size = B, C)

指令名称类型操作码BCASELFiABC0x0COpArgROpArgK目标寄存器 idx

B:1 C:0x100 A:2 SELF

把寄存器中对象(索引由 B 指定)和常量表中方法(索引由 C 指定)拷贝到相邻的两个目标寄存器中,起始目标寄存器的索引由 A 指定。

208e9bf3f407ed0c0e9452d9f09a67fb.png

公式:

R(A+1) := R(B)

R(A) := R(B)[RK(C)]

指令名称类型操作码BCAADDiABC0x0DOpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 ADD

对两个寄存器或常量值(索引由 B 和 C 指定)进行相加,并将结果放入另一个寄存器中(索引由 A 指定)。

81db96cb07010e50265b3d17493e9e10.png

公式:R(A) := RK(B) + RK(C)

指令名称类型操作码BCASUBiABC0x0EOpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 SUB

对两个寄存器或常量值(索引由 B 和 C 指定)进行相减,并将结果放入另一个寄存器中(索引由 A 指定)

公式:

R(A) := RK(B) - RK(C)

指令名称类型操作码BCAMULiABC0x0FOpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 MUL

对两个寄存器或常量值(索引由 B 和 C 指定)进行相乘,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) * RK(C)

指令名称类型操作码BCAMODiABC0x10OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 MOD

对两个寄存器或常量值(索引由 B 和 C 指定)进行求摸运算,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) % RK(C)

指令名称类型操作码BCAPOWiABC0x11OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 POW

对两个寄存器或常量值(索引由 B 和 C 指定)进行求幂运算,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) ^ RK(C)

指令名称类型操作码BCADIViABC0x12OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 DIV

对两个寄存器或常量值(索引由 B 和 C 指定)进行相除,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) / RK(C)

指令名称类型操作码BCAIDIViABC0x13OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 IDIV

对两个寄存器或常量值(索引由 B 和 C 指定)进行相整除,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) // RK(C)

指令名称类型操作码BCABANDiABC0x14OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 BAND

对两个寄存器或常量值(索引由 B 和 C 指定)进行求与操作,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) & RK(C)

指令名称类型操作码BCABORiABC0x15OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 BOR

对两个寄存器或常量值(索引由 B 和 C 指定)进行求或操作,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) | RK(C)

指令名称类型操作码BCABXORiABC0x16OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 BXOR

对两个寄存器或常量值(索引由 B 和 C 指定)进行求异或操作,并将结果放入另一个寄存器中(索引由 A 指定)

公式:R(A) := RK(B) ~ RK(C)

指令名称类型操作码BCASHLiABC0x17OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 SHL

索引由 B 指定的寄存器或常量值进行左移位操作(移动位数的索引由 C 指定的寄存器或常量值),并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) << RK(C)

指令名称类型操作码BCASHRiABC0x18OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:4 SHR

索引由 B 指定的寄存器或常量值进行右移位操作(移动位数的索引由 C 指定的寄存器或常量值),并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := RK(B) >> RK(C)

指令名称类型操作码BCAUNMiABC0x19OpArgROpArgN目标寄存器 idx

B:1 C A:3 UNM

对寄存器(索引由 B 指定)进行取负数操作,并将结果放入另一个寄存器中(索引由 A 指定)。

公式:R(A) := - R(B)

指令名称类型操作码BCABNOTiABC0x1AOpArgROpArgN目标寄存器 idx

B:1 C A:3 BNOT

对寄存器(索引由 B 指定)进行取反操作,并将结果放入另一个寄存器中(索引由 A 指定)。

b3745c5cfab0f4cba2b894dc8d0d749b.png

公式:R(A) := ~ R(B)

指令名称类型操作码BCANOTiABC0x1BOpArgROpArgN目标寄存器 idx

B:1 C A:3 NOT

对寄存器(索引由 B 指定)进行求非操作,并将结果放入另一个寄存器中(索引由 A 指定)。

e727cd74af6cce9171c587240dd1b52e.png

公式:R(A) := not R(B)

指令名称类型操作码BCALENiABC0x1COpArgROpArgN目标寄存器 idx

B:1 C A:3 LEN

对寄存器(索引由 B 指定)进行求长度操作,并将结果放入另一个寄存器中(索引由 A 指定)。

c82a19818aea3a7fb23eee69e41f70ef.png

公式:R(A) := length of R(B)

指令名称类型操作码BCACONCATiABC0x1DOpArgROpArgR目标寄存器 idx

B:2 C:4 A:1 CONCAT

将连续 n 个寄存器(起始索引和终止索引由 B 和 C 指定)里的值进行拼接,并将结果放入另一个寄存器中(索引由 A 指定)。

41597912e49afe72c5823dda8aba49d6.png

公式:R(A) := R(B) .. ... .. R(C)

指令名称类型操作码sBxAJMPiAsBx0x1EOpArgR目标寄存器 idx

sBx:-1 A JMP

当 sBx 不为 0 时,进行无条件跳转,执行 pc = pc + sBx(sBx 为-1,表示将当前指令再执行一次 注:这将是一个死循环)

sBx:0 A:0x001 JMP;

当 sBx 为 0 时(继续执行后面指令,不跳转),用于闭合处于开启状态的 Upvalue(即:把即将销毁的局部变量的值复制出来,并更新到某个 Upvalue 中)。

当前闭包的某个 Upvalue 值的索引由 A 指定:

指令名称类型操作码BCAEQiABC0x1FOpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:1 EQ

寄存器或常量表(索引由 B 指定)是否等于寄存器或常量表(索引由 C 指定),若结果等于操作数 A,则跳过下一条指令。

acf576752028c4123c0c395d5302f6e3.png

公式:if ((RK(B) == RK(C)) pc++

指令名称类型操作码BCALTiABC0x20OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:1 LT

寄存器或常量表(索引由 B 指定)是否小于寄存器或常量表(索引由 C 指定),若结果等于操作数 A,则跳过下一条指令。

公式:if ((RK(B) < RK(C)) pc++

指令名称类型操作码BCALEiABC0x21OpArgKOpArgK目标寄存器 idx

B:0x001 C:0x100 A:1 LE

寄存器或常量表(索引由 B 指定)是否小于等于寄存器或常量表(索引由 C 指定),若结果等于操作数 A,则跳过下一条指令。

公式:if ((RK(B) <= RK(C)) pc++

指令名称类型操作码BCATESTiABC0x22OpArgNOpArgU目标寄存器 idx

B C:0 A:1 TEST

判断寄存器(索引由 A 指定)中的值转换为 bool 值后,是否和操作数 C 表示的 bool 值一致,若结果不一致,则跳过下一条指令。

7899d0adabe581dfd085ec29e767ac3f.png

公式:

if not (R(A) <=> C) pc++

注:<=>表示按 bool 值比较

指令名称类型操作码BCATESTSETiABC0x23OpArgROpArgU目标寄存器 idx

B:3 C:0 A:1 TESTSET

判断寄存器(索引由 B 指定)中的值转换为 bool 值后,是否和操作数 C 表示的 bool 值一致,若结果一致,将寄存器(索引由 B 指定)中的值复制到寄存器中(索引由 A 指定),否则跳过下一条指令。

b387ea5043c1797dcda76cdc13066313.png

公式:

if (R(B) <=> C)   R(A) := R(B)else   pc++ 

注:<=>表示按 bool 值比较

指令名称类型操作码BCACALLiABC0x24OpArgUOpArgU目标寄存器 idx

B:5 C:4 A:0 CALL

被调用函数位于寄存器中(索引由 A 指定),传递给被调用函数的参数值也在寄存器中,紧挨着被调用函数,参数个数为操作数 B 指定。

① B==0,接受其他函数全部返回来的参数

② B>0,参数个数为 B-1

函数调用结束后,原先存放函数和参数值的寄存器会被返回值占据,具体多少个返回值由操作数 C 指定。

① C==0,将返回值全部返回给接收者

② C==1,无返回值

③ C>1,返回值的数量为 C-1

01eeee9368cd6e592b0c708f249310fd.png

公式:R(A), ... ,

指令名称类型操作码BCATAILCALLiABC0x25OpArgUOpArgU目标寄存器 idx

函数调用一般通过调用栈来实现。用这种方法,每调用一个函数都会产生一个调用帧。

如果调用层次太深(如递归),容易导致栈溢出。尾递归优化则可以让我们发挥递归函数调用威力的同时,避免调用栈溢出。利用这种优化,被调函数可以重用主调函数的调用帧,因此可有效缓解调用栈溢出症状。不过该优化只适合某些特定情况。

如:return f(args) 会被编译器优化成 TAILCALL 指令,公式:return R(A)(R(A+1), ... , R(A+B-1))

指令名称类型操作码BCARETURNiABC0x26OpArgUOpArgN目标寄存器 idx

B:4 C A:2 RETURN

把存放在连续多个寄存器里的值返回给父函数,其中第一个寄存器的索引由操作数 A 指定,寄存器数量由操作数 B 指定,操作数 C 没有使用,需要将返回值推入栈顶:

① B==1,不需要返回任何值

② B > 1,需要返回 B-1 个值;这些值已经在寄存器中了,只用再将它们复制到栈顶即可

③ B==0,一部分返回值已经在栈顶了,只需将另一部分也推入栈顶即可

5c2a0efec8badcb7ed1f573937c93844.png

公式:return R(A),...,R(A+B-2)

指令名称类型操作码sBxAFORLOOPiAsBx0x27OpArgR目标寄存器 idx

数值 for 循环:用于按一定步长遍历某个范围内的数值 如:for i=1,100,2 do f() end // 初始值为 1,步长为 2,上限为 100

该指令先给 i 加上步长,然后判断 i 是否在范围之内。若已经超出范围,则循环结束;若为超出范围,则将数值拷贝给用户定义的局部变量,然后跳转到循环体内部开始执行具体的代码块。

6c230c4911cdebb6806f1da5635ab0aa.png

公式:

R(A) += R(A+2)if R(A) = R(A+1)    pc+=sBx    R(A+3)=R(A)

注:当步长为正数时=为<=

当步长为负数时=为>=

指令名称类型操作码sBxAFORPREPiAsBx0x28OpArgR目标寄存器 idx

数值 for 循环:用于按一定步长遍历某个范围内的数值 如:for i=1,100,2 do f() end // 初始值为 1,步长为 2,上限为 100。

该指令的目的是在循环之前预先将 i 减去步长(得到-1),然后跳转到 FORLOOP 指令正式开始循环:

44e9c669718285361d7009c4ba7abb70.png

公式:

R(A)-=R(A+2)

pc+=sBx

指令名称类型操作码BCATFORCALLiABC0x29OpArgNOpArgU目标寄存器 idx

通用 for 循环:for k,v in pairs(t) do print(k,v) end

编译器使用的第一个特殊变量(generator):f 存放的是迭代器,其他两个特殊变量(state):s、(control):var 来调用迭代器,把结果保存在用户定义的变量 k、v 中。

9dce70ee751830dd31bd69c2651ce2af.png

公式:R(A+3),...,R(A+2+C) := R(A)(R(A+1),R(A+2))

指令名称类型操作码sBxATFORLOOPiAsBx0x2AOpArgR目标寄存器 idx

通用 for 循环:for k,v in pairs(t) do print(k,v) end

若迭代器返回的第一个值(变量 k)不是 nil,则把该值拷贝到(control):var,然后跳转到循环体;若为 nil,则循环结束。

0457ca52224f0f61d8e77b0b70c80d03.png

公式:

if R(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 使用。

42fe8c88445343284ec31f308de31fe9.png

公式:

R(A)[(C-1)*FPF+i] := R(A+i)

1 <= i <= B

指令名称类型操作码BxACLOSUREiABx0x2COpArgU目标寄存器 idx

把当前 Lua 函数的子函数原型实例化为闭包,放入由操作数 A 指定的寄存器中子函数原型来自于当前函数原型的子函数原型表,索引由操作数 Bx 指定。

下图为将 prototypes 表中索引为 1 的 g 子函数,放入索引为 4 的寄存器中:

0d51c14c0db438f996862d0ac5e2064a.png

公式:R(A) := closure(KPROTO[Bx])

指令名称类型操作码BCAVARARGiABC0x2DOpArgUOpArgN目标寄存器 idx

把传递给当前函数的变长参数加载到连续多个寄存器中。

其中第一个寄存器的索引由操作数 A 指定,寄存器数量由操作数 B 指定,操作数 C 没有使用,操作数 B 若大于 1,表示把 B-1 个 vararg 参数复制到寄存器中,否则只能等于 0。

4bdfbf8399c3bce2d64aa04020fd101a.png

公式:R(A),R(A+1),...R(A+B-2)=vararg

指令名称类型操作码AxEXTRAARGiAx0x2EOpArgU

Ax:67108864 EXTRAARG

Ax 有 26bits,用来指定常量索引,可存放最大无符号整数为 67108864,可满足大部分情况的需要了。

参考

  • 《自己动手实现 Lua》源代码
  • Lua 设计与实现--虚拟机篇
  • Lua 5.3 Bytecode Reference
  • Lua 源码解析

作者:nicochen,腾讯 IEG 游戏开发工程师

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值