Lua虚拟机会将Lua语句进行解析,解析为Lua虚拟机指令。Lua虚拟机指令有如下四十条(在lopcodes.h有定义)。有时我们需要在lua层面分析lua虚拟机的行为,那么解析lua虚拟机指令将是非常有用的。 我们可以通过luac来将lua脚本生成lua虚拟机指令。

Lua虚拟机指令

Lua的指令使用一个32bit的unsigned interger表示。所有指令都在lopcodes.h中。根据指令参数的不同,可以将所有指令分为4类:

Lua虚拟机指令类型

除了sBx之外,所有的指令参数都是unsigned integer类型。sBx可以表示负数,但表示方法非常特殊。ABC一般用力啊存放指令操作数据的地址,而地址可以分成3中:

  • 寄存器id
  • 常量表id
  • upvalue id

Lua使用当前函数的stack作为寄存器使用,寄存器id从0开始。当前函数的stack与寄存器数组是相同的该你那。stack(n)就是register(n)

每一个函数prototype中都有一个upvalue描述表,用于存放在编译过程中确定的本函数所使用的upvalue的描述。在运行期,通过OP_CLOSURE指令创建一个closure时,会根据prototype中的描述,为这个closure初始化upvalue表。upvalue本身不需要使用名称,而是通过id进行访问。

A被大多数指令用来指定计算结果的目标寄存器地址。很多指令使用B或C同时存放寄存器地址和常量地址,并通过最左面的一个bit来区分。在指令生成阶段,如果B或C需要引用的常量地址超出了表示范围,则首先会生成指令将常量装载到临时寄存器,然后再将B或C改为使用该寄存器地址。

再lopcode.h中,对于每个指令都有描述。在分析指令生成过程中,我们使用luac来现实实例代码生成的指令。luac的具体使用方式为

Lua虚拟机指令有如下40条,在lopcode.h中的定义摘录如下:

UpValue 与 Glabals

在编译期,若果要访问变量a时,会依照以下顺序决定变量a的类型:

  • a是当前函数的local变量
  • a是外层函数的local变量,那么a时当前函数的upvalue
  • a是全局变量

local变量本身就存在于当前的register中,所有的指令都可以直接使用它的id来访问。对于upvalue,lua有专门的指令负责获取和设置。全局变量表放到最外层函数的名字为“_ENV”来访问。如果你访问全局变量a,相当于编译期帮你改成了_ENV.a来进行访问。

nameargsdesc
OP_GETUPVALA B CR(A) := UpValue[B]
OP_SETUPVALA BUpValue[B] := R(A)
OP_GETTABUPA B CR(A) := UpValue[B][RK(C)]
OP_SETTABUPA B CUpValue[A][RK(B)] := RK(C)

GETUPVAL将B作为索引的upvalue的值装在到A寄存器中。SETUPVAL将A寄存器的值保存到B为索引的upvalue中。

GETTABUP将B作为索引的upvalue当作一个table,并将C做为索引的寄存器或者常量当作key获取的值放入寄存器A。SETTABUP将A为索引的upvalue当作一个table,将C寄存器或者常量的值以B寄存器或常量为key,存入table

Table

nameargsdesc
OP_NEWTABLEA B CR(A) := {} (size = B,C)

NEWTABLE在寄存器A处创建一个table对象。B和C分别用来存储这个table数组部分和hash部分的初始大小。初始大小是在编译期计算出来并生成到这个指令中的,目的是使接下来对table的初始化填充不会造成rehash而影响效率。B和C使用“floating point byte”的方法来表示成(eeeeexxx)的二进制形式,其实际值为(1xxx) * 2^(eeeee-1)。

nameargsdesc
OP_SETLISTA B CR(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B

SETLIST用来配合NEWTABLE,初始化表的数组部分使用的。A为保存待设置表的寄存器,SETLIST要将A下面紧接着的寄存器列表(1–B)中的值逐个设置给表的数组部分。

当表需要初始化数组元素数量比较小的情况下,例如:

第1行先用NEWTABLE构建一个具有3个数组元素的表,让到寄存器0中;然后使用3个LOADK向下面3个寄存器装入常量1;最后使用SETLIST设置表的1~3为寄存器1~寄存器3。

如果需要创建一个很大的表,其中包含很多的数组元素,使用如上方法就会遇到一个问题。将这些指按顺序放到寄存器时,会超出寄存器的范围。解决的办法就是按照一个固定大小,将这些数组元素分批进行设置。在Lua中,每批的数量由lopcodes.h中的LFIELDS_PER_FLUSH定义,数量为50。所以,大数量的设置会按照50个一批,先将值设置到表下面的寄存器,然后设置给对应的表项。C代表的就是这一次调用SETLIST设置的是第几批。回到上面的例子,因为只有3个表项,所以1批就搞定了,C的值为1。

下面是一个大表的设置:

可以看到,这个表的初始化使用了两个SETLIST指令。第一个处理前50个,C为1,设置id从(C-1)*50 + 1开始,也就是1。第二个处理余下的3个,C为2,设置的id从(C-1)*50 + 1开始,也就是51。

如果数据非常大,导致需要的批次超出了C的表示范围,那么C会被设置成0,然后在SETLIST指令后面生成一个EXTRAARG指令,并用其Ax来存储批次。这与前面说到的LOADKX的处理方法一样,都是为处理超大数据服务的。

如果使用核能产生多个返回值的表达式(… 和 函数调用)初始化数组项,如果这个初始化不是表构造的最后一项,那么只有第一个返回值会被设置到数组项;如果是最后一项,那么SETLIST中的B会被设置为0,表示从A+1到当前栈顶都用来设置。

SETLIST只负责初始化表的数组部分,对于hash部分,还是通过SETTABLE来初始化。

nameargsdesc
OP_GETTABLEA B CR(A) := R(B)[RK(C)]
OP_SETTABLEA B CR(A)[RK(B)] := RK(C)

GETTABLE使用C表示的key,将寄存器B中的表项值获取到寄存器A中。SETTABLE设置寄存器A的B项为C代表的值。

Arithmetic

nameargsdesc
OP_ADDA B CR(A) := RK(B) + RK(C)
OP_SUBA B CR(A) := RK(B) – RK(C)
OP_MULA B CR(A) := RK(B) * RK(C)
OP_DIVA B CR(A) := RK(B) / RK(C)
OP_MODA B CR(A) := RK(B) % RK(C)
OP_POWA B CR(A) := RK(B) ^ RK(C)

上表中的指令都是lua本身的二元操作符——对应的标准3地址指令。B和C两个操作数计算的结果存入A中。

nameargsdesc
OP_UNMA BR(A) := -R(B)
OP_NOTA BA B R(A) := not R(B)

商标中指令对应 ‘-’ 和 ‘not’ 操作符,表示将B取反或not后放入A中。

在编译和指令生成阶段,lua还支持所有一元和二元操作符表达式的常量表达式折叠优化。也就是说如果操作数都是数字常量,可以在编译期计算出结果,就直接使用这个结果值,而不生成计算指令。

nameargsdesc
OP_CONCATA B CR(A) := R(B).. … ..R(C)

LEN直接对应 ‘#’ 操作符,返回B对象的长度,并保存到A中。

nameargsdesc
OP_CONCATA B CR(A) := R(B).. … ..R(C)

CONCAT对应字符串连接操作

Function

nameargsdesc
OP_CALLA B CA B C   R(A), … ,R(A+C-2) := R(A)(R(A+1), … ,R(A+B-1))

CALL执行一个函数调用。寄存器A中存放函数对象,所有参数按顺序防止在A后面的寄存器中。B-1表示参数个数。如果参数列表的最后一个表达式是变长的,则B会设置为0,表示使用A+1到当前栈顶作为参数。函数调用返回值会按照顺序存放在从寄存器A开始的C-1个寄存器中。如果C为0,则表示返回值的个数由函数决定。

nameargsdesc
OP_RETURNA Breturn R(A), … ,R(A+B-2)

RETURE将返回结果存放到寄存器A到寄存器A+B-2中。如果返回的为变长表达式,则B会被设置为0,表示将寄存器A到当前栈顶的所有值返回。

nameargsdesc
OP_CLOSUREA BxR(A) := closure(KPROTO[Bx])

CLOSURE为指定的函数prototype创建一个closure,并将这个closure保存到寄存器A中。Bx用来指定函数prototype的id。

nameargsdesc
OP_VARARGA BR(A), R(A+1), …, R(A+B-2) = vararg 

VARARG直接对应’…’运算符。VARARG拷贝B-1个参数到从A开始的寄存器中,如果不足,使用nil补充。如果B为0,表示拷贝实际的参数数量。

nameargsdesc
OP_SELFA B CR(A+1) := R(B); R(A) := R(B)[RK(C)]

SELF是专门为“:”运算符准备的指令。从寄存器B表示的table中,获取出C作为key的closure,存入寄存器A中,然后将table本身存入到寄存器A+1中,为接下来调用这个closure做准备。

看一下与上面语法等价的表示方法生成的指令:

比使用“:”操作符多使用了一个指令。所以,如果需要使用这种面向对象调用的语义时,应该尽量使用”:”。

关系与逻辑

nameargsdesc
OP_JMPA sBxpc+=sBx; if (A) close all upvalues >= R(A) + 1

JMP执行一个跳转,sBx表示跳转的偏移位置,被加到当前指向下一指令的指令指针上。如果sBx为0,表示没有任何跳转;1表示跳过下一个指令;-1表示重新执行当前指令。如果A>0,表示需要关闭所有从寄存器A+1开始的所有local变量。实际执行的关闭操作只对upvalue有效。

JMP最直接的使用就是对应lua5.2新加入的goto语句。Lua5.2中取出了以前专门处理关闭upvalue的指令CLOSE,而把这个功能加入到JMP中。

nameargsdesc
OP_EQA B Cif ((RK(B) == RK(C)) ~= A) then pc++
OP_LTA B Cif ((RK(B) <  RK(C)) ~= A) then pc++
OP_LEA B Cif ((RK(B) <= RK(C)) ~= A) then pc++

关系指令对RK(B)和RK(C)进行比较,然后将比较结果与A指定的boolean值进行比较,来决定最终的boolean值。A在这里为每个关系指令提供了两种比较目标,满足和不满足。比如OP_LT何以用来实现“<”和“>”。

nameargsdesc
OP_TESTA Cif not (R(A) <=> C) then pc++
OP_TESTSETA B Cif (R(B) <=> C) then R(A) := R(B) else pc++

逻辑指令用于实现and和or逻辑运算符,或者在条件语句中判断一个寄存器。TESTSET将寄存器B转化成一个boolean值,然后与C进行比较。如果不相等,跳过后面的JMP指令。否则将寄存器B的值赋给寄存器A,然后继续执行。TEST是TESTSET的简化版,不需要赋值操作。

Look

Lua5.2种除了for循环之外,其他的各种循环都使用关系和逻辑指令,配合JMP指令来完成。

第二行使用LT对寄存器0和敞亮10进行比较,如果小于成立,跳过第三行的JMP,运行第四行的ADD指令,将a加1,然后运行第五行的JMP,跳转回第二行,重新判断条件。如果小于不成立,则直接运行下一个JMP指令,跳转到第六行结束。

对于for循环,Lua5.2使用了两套专门的指令,分别对应numeric for loop和generic for loop。

nameargsdesc
OP_FORLOOPA sBxR(A)+=R(A+2);
if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }
OP_FORPREPA sBxR(A)-=R(A+2); pc+=sBx

Numeric for loop内部使用了3个局部变量来控制循环,他们分别是”for index”,“for limit”和“for step”。“for index”用作存放初始值和循环计数器,“for limit”用作存放循环上限,“for step”用作存放循环步长。对于上面的程序,三个值分别是1,10和1。这三个局部变量对于使用者是不可见得,我们可以在生成代码的locals表中看到这3个局部变量,他们的有效范围为第五行道第八行,也就是整个for循环。还有一个使用到的局部变量,就是使用者自己指定的计数器,上例中为”i”。我们可以看到,这个局部变量的有效范围为6~7行,也就是循环的内部。这个变量在每次循环时都被设置成”for index”变量来使用。

上例中2~4行初始化循环使用的3个内部局部变量。第五行FORPREP用于准备这个循环,将for index减去一个for step,然后跳转到第七行。第七行的FORLOOP将for index加上一个for step,然后与for limit进行比较。如果小于等于for limit,则将i设置成for index,然后跳回第六行。否则就退出循环。我们可以看到,i并不用于真正的循环计数,而只是在每次循环时被赋予真正的计数器for index的值而已,所以在循环中修改i不会影响循环计数。

nameargsdesc
OP_TFORCALLA CR(A+3), … ,R(A+2+C) := R(A)(R(A+1), R(A+2));
OP_TFORLOOPA sBxif R(A+1) ~= nil then { R(A)=R(A+1); pc += sBx }

Generic for loop内部也使用了3个局部变量来控制循环,分别是”for generator”,“for state”和“for control”。for generator用来存放迭代使用的closure,每次迭代都会调用这个closure。for state和for control用于存放传给for generator的两个参数。Generic for loop还使用自定义的局部变量i,v,用来存储for generator的返回值。

上例中1~3行使用in后面的表达式列表(1,2,3)初始化3个内部使用的局部变量。第四行JMP调转到第六行。TFORCALL教用寄存器0(for generator)中的closure,传入for state和for control,并将结果返回给自定义局部变量列表i和v。第七行调用TFORLOOP进行循环条件判断,判断i是否为空。如果不为空,将i的值赋给for control,然后跳转到第五行,进行循环。

发表评论

电子邮件地址不会被公开。 必填项已用*标注