探索Lua5.2内部实现:虚拟机指令(7) 关系和逻辑指令

原文链接

1
2
name	args	desc
OP_JMP A sBx pc+=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语句:

1
2
::l::
```bash

goto l;
1 [1] JMP 0 -1 ; to 1
2 [2] RETURN 0 1

1
2
3
4
5
6
这是一个无限循环。第一行JMP的sBx为-1,表示重新执行JMP。
```lua
do
local a;
function f() a = 1 end
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main <test.lua:0,0> (5 instructions at 0x80048eb0)
0+ params, 2 slots, 1 upvalue, 1 local, 1 constant, 1 function
1 [2] LOADNIL 0 0
2 [3] CLOSURE 1 0 ; 0x80049128
3 [3] SETTABUP 0 -1 1 ; _ENV "f"
4 [3] JMP 1 0 ; to 5
5 [4] RETURN 0 1
constants (1) for 0x80048eb0:
1 "f"
locals (1) for 0x80048eb0:
0 a 2 5
upvalues (1) for 0x80048eb0:
0 _ENV 1 0

function <test.lua:3,3> (3 instructions at 0x80049128)
0 params, 2 slots, 1 upvalue, 0 locals, 1 constant, 0 functions
1 [3] LOADK 0 -1 ; 1
2 [3] SETUPVAL 0 0 ; a
3 [3] RETURN 0 1
constants (1) for 0x80049128:
1 1
locals (0) for 0x80049128:
upvalues (1) for 0x80049128:
0 a 1 0

上面的代码在do block中创建了一个局部变量a,并且a作为upvalue在函数f中被引用到。到退出do block是,a会退出他的有效域,并且关闭他对应的upvalue。Lua5.2中去除了以前专门处理关闭upvalue的指令CLOSE,而把这个功能加入到了JMP中。所以,生成的指令第四行的JMP在这里没有执行跳转,而只是为了关闭a的upvalue。
JMP其他的功能就是配合逻辑和关系指令(统称为test指令),实现程序的条件跳转。每个test辑指令与JMP搭配,都会将接下来生成的指令分为两个集合,满足条件的为true集合,否则为false集合。当test条件满足时,指令指针回+1,跳过后面紧跟的JMP指令,然后继续执行。当test条件不满足时,则继续执行,也就到了JMP,然后跳转到分支代码。

1
2
3
4
name	args	desc
OP_EQ A B C if ((RK(B) == RK(C)) ~= A) then pc++
OP_LT A B C if ((RK(B) <  RK(C)) ~= A) then pc++
OP_LE A B C if ((RK(B) <= RK(C)) ~= A) then pc++

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

1
2
local a,b,c;
a = b < c;

1
2
3
4
5
6
1       [1]     LOADNIL         0 2
2 [2] LT 1 1 2
3 [2] JMP 0 1 ; to 5
4 [2] LOADBOOL 0 0 1
5 [2] LOADBOOL 0 1 0
6 [2] RETURN 0 1

第二行的LT对寄存器1和2进行LT比较,如果结果为true,则继续执行后面的JMP,跳转到第五行的LOADBOOL,将寄存器0赋值为true;如果结果为false,则跳过后面的JMP,执行第四行的LOADBOOL,将寄存器0赋值为false。我们前面讲过关于LOADBOOL,第四行执行后会跳过第五行的赋值。

1
2
3
name	args	desc
OP_TEST A C if not (R(A) <=> C) then pc++
OP_TESTSET A B C if (R(B) <=> C) then R(A) := R(B) else pc++

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

1
2
local a,b,c;
a = b and c;

1
2
3
4
5
1       [1]     LOADNIL         0 2
2 [2] TESTSET 0 1 0
3 [2] JMP 0 1 ; to 5
4 [2] MOVE 0 2
5 [2] RETURN 0 1

第二行的TESTSET将寄存器1的值与false比较。如果不成立,跳过JMP,执行第四行的MOVE,将寄存器2的值赋给寄存器0。否则,将寄存器1的值赋给寄存器0;然后执行后面的JMP。
上面的代码等价于

1
2
3
4
5
if b then
a = c
else
a = b
end

-------------本文结束 感谢阅读-------------