Skip to content

Latest commit

 

History

History
202 lines (140 loc) · 10.6 KB

ch04-循环类指令.md

File metadata and controls

202 lines (140 loc) · 10.6 KB

##循环类指令

###相关指令

OP_FORLOOP,/*   A sBx   R(A)+=R(A+2);if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/*   A sBx   R(A)-=R(A+2); pc+=sBx               */
OP_TFORLOOP,/*  A C R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));if R(A+3) ~= nil then R(A+2)=R(A+3) else pc++   */

###理论基础

###for循环指令 for循环分为两类,一类是数值类的for循环,循环变量以某个步长来递增,直到某个值时退出for循环;另一种是为更通用的循环操作,每次循环开始时调用一个函数,只有当返回值不为Nil时才继续进行循环 .

涉及数字类循环的指令是OP_FORLOOP和OP_FORPREP,其中OP_FORPREP指令负责循环的初始化操作,它只会在循环开始的时候执行一次,而OP_FORLOOP则负责循环终止条件的判断,它会在每次循环操作开始前执行一次,如果已经不满足循环的执行条件,将会终止循环.

这两个指令需要操作在函数栈上的连续4个数据,同时它们必须是数字类型的数据.其中R(A+1)是循环的结束值,R(A+2)是循环变量每次递增的步长值,R(A)和RA(3)同样都是循环变量,只不过R(A)用于循环变量的初始化和计算的结果,当满足继续循环下去的条件时,再将R(A)赋值给R(A+3),在循环体中实际使用到的循环变量是RA(3).

在循环开始的准备阶段,首先执行OP_FORPREP指令,将循环变量减去步长值,需要注意的是,循环变量最开始的赋值并不在这里执行.然后pc指针将根据sBx参数跳转到OP_FORLOOP指令中,再让循环变量加上步长值,也就是一减一加最后在初始化阶段并没有让循环变量的值发生变化.之所以这样做,是因为OP_FORLOOP指令会在每次进入循环体中首先加上步长值再进行循环条件的判断,为了配合这一操作所以在OP_FORPREP中首先减去了步长值.OP_FORLOOP指令在加上步长值之后会对比循环终止的结束值与当前循环变量的大小,如果仍然可以继续进行循环操作,将把pc指针跳转回循环体的第一段指令,也就是OP_FORPREP的下一条指令,并且将R(A+3)使用R(A)来替换来进行下一次的循环.

这里使用的测试代码是这样的:

local a = 0; for i = 1, 100, 5 do a = a + i end;

使用ChunkSpy之后反编译的结果如下:

; source chunk: (interactive mode)
; x86 standard (32-bit, little endian, doubles)

; function [0] definition (level 1)
; 0 upvalues, 0 params, 5 stacks
.function  0 0 2 5
.local  "a"  ; 0
.local  "(for index)"  ; 1
.local  "(for limit)"  ; 2
.local  "(for step)"  ; 3
.local  "i"  ; 4
.const  0  ; 0
.const  1  ; 1
.const  100  ; 2
.const  5  ; 3
[1] loadk      0   0        ; 0
[2] loadk      1   1        ; 1
[3] loadk      2   2        ; 100
[4] loadk      3   3        ; 5
[5] forprep    1   1        ; to [7]
[6] add        0   0   4
[7] forloop    1   -2       ; to [6] if loop
[8] return     0   1
; end of function

在这里,会创建几个额外的局部变量,可以看到local(1)是循环变量,local(2)是循环的结束值,local(3)是循环的步长,local(4)是循环变量i,在开始循环之前,local(1)到local(3)会依次将它们进行赋值的初始化操作,但是变量i并没有做初始化操作,如前面所说,循环变量的真正的初始化操作是在指令forloop中进行的.

第5行的forprep指令首先将循环变量减去循环步长,再跳转到第7行的forloop操作中,进行循环变量与循环结束值的判断,如果满足继续循环的条件,那么才真正赋值循环变量i为这一次循环的值,同时跳转到第6行的循环体中执行循环的操作.每次执行完循环再次进入forloop指令中判断是否可以终止循环,以此类推.

如果不是数值类的for循环,那么就是对一个table进行遍历的for循环,此时使用的是OP_TFORLOOP指令:

OP_TFORLOOP,/*  A C R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));if R(A+3) ~= nil then R(A+2)=R(A+3) else pc++   */

其中,R(A)是每次循环时调用的函数,可以称为gererator,R(A+1)是当前循环的状态,R(A+2)是循环变量,可以理解为R(A+1)是每次循环时都不会变化的值,也就是循环变量的table,而R(A+2)则是每次循环时都会发生变化的值,也就是这次遍历到的table的索引值.返回的值放在从R(A+3)到R(R+2+C)的变量中,因此该指令的参数C表示返回值的数量,至少为1.如果返回的R(A+3)不为Nil,那么将赋值给R(A+2)用于下一次循环,否则就将PC指针递增,跳过紧跟着的OP_JMP指令,退出循环,而紧跟在后面的OP_JMP指令用于在满足继续循环条件的情况下跳转回循环体继续执行循环.换言之,每个OP_TFORLOOP指令都会紧跟着一条OP_JMP指令,用于在跳转回去继续执行循环.

这里使用的Lua测试代码是:

for k,v in pairs(t) do print(k,v) end

ChunkSpy输出:

; function [0] definition (level 1)
; 0 upvalues, 0 params, 8 stacks
.function  0 0 2 8
.local  "(for generator)"  ; 0
.local  "(for state)"  ; 1
.local  "(for control)"  ; 2
.local  "k"  ; 3
.local  "v"  ; 4
.const  "pairs"  ; 0
.const  "t"  ; 1
.const  "print"  ; 2
[01] getglobal  0   0        ; pairs
[02] getglobal  1   1        ; t
[03] call       0   2   4
[04] jmp        4            ; to [9]
[05] getglobal  5   2        ; print
[06] move       6   3
[07] move       7   4
[08] call       5   3   1
[09] tforloop   0       2    ; to [11] if exit
[10] jmp        -6           ; to [5]
[11] return     0   1
; end of function

1-4行:得到Lua的库函数pairs,pairs的参数t,调用pairs函数的结果是得到这个函数返回的genenator函数,存放在R(A)中,在这里就是local(0).紧跟着跳转到9行调用generator函数,赋值给循环变量,做循环条件的判断.

5-8:循环体.首先得到库函数print,其次注意到的是使用两个move指令分别把local(3),local(4)赋值给local(6),local(7),再使用它们调用print函数.而这里的local(3),local(4)就是后面的tforloop函数在调用generator函数之后的返回值.

9:tforloop指令的A参数是0,C参数是2,也就是说,这里的generator函数返回两个结果,存放在local(3)以及local(4),这与前面的分析是一致的.

10:当上一条OP_TFORLOOP指令继续循环的条件满足,也就是返回的R(A+3)不为Nil时,跳转回循环体再次执行循环.

11:这里是这系列循环代码的下一条指令,当需要退出循环时直接跳转到这里终止循环继续后面的执行.

有了以上的准备,可以来看看Lua中处理for循环语句的代码:

(lparser.c)
1112 static void forstat (LexState *ls, int line) {
1113   /* forstat -> FOR (fornum | forlist) END */
1114   FuncState *fs = ls->fs;
1115   TString *varname;
1116   BlockCnt bl;
1117   enterblock(fs, &bl, 1);  /* scope for loop and control variables */
1118   luaX_next(ls);  /* skip `for' */
1119   varname = str_checkname(ls);  /* first variable name */
1120   switch (ls->t.token) {
1121     case '=': fornum(ls, varname, line); break;
1122     case ',': case TK_IN: forlist(ls, varname); break;
1123     default: luaX_syntaxerror(ls, LUA_QL("=") " or " LUA_QL("in") " expected");
1124   }
1125   check_match(ls, TK_END, TK_FOR, line);
1126   leaveblock(fs);  /* loop scope (`break' jumps to this point) */
1127 }

可以看到,首先将解析第一个循环变量到varname中,然后根据下一个token是"="号则进入数值类for循环的处理,如果是","或者"in"则进入list类for循环的处理,其他的情况将报错.

首先看数值类指令的处理代码:

(lparser.c)
1067 static void fornum (LexState *ls, TString *varname, int line) {
1068   /* fornum -> NAME = exp1,exp1[,exp1] forbody */
1069   FuncState *fs = ls->fs;
1070   int base = fs->freereg;
1071   new_localvarliteral(ls, "(for index)", 0);
1072   new_localvarliteral(ls, "(for limit)", 1);
1073   new_localvarliteral(ls, "(for step)", 2);
1074   new_localvar(ls, varname, 3);
1075   checknext(ls, '=');
1076   exp1(ls);  /* initial value */
1077   checknext(ls, ',');
1078   exp1(ls);  /* limit */
1079   if (testnext(ls, ','))
1080     exp1(ls);  /* optional step */
1081   else {  /* default step = 1 */
1082     luaK_codeABx(fs, OP_LOADK, fs->freereg, luaK_numberK(fs, 1));
1083     luaK_reserveregs(fs, 1);
1084   }
1085   forbody(ls, base, line, 1, 1);
1086 }

这里初始化了三个特殊的局部变量,分别命名为特殊的名字:"(for index)","(for limit)","(for step)",这不是Lua代码能正常声明的变量名字,然后紧跟着以循环变量的名字再创建一个局部变量,这四个变量依次在当前栈的0到3的位置.

紧跟着,依次调用exp1函数得到循环初始化值和结束值到R(A)和R(A+1)中.在没有第三个步长变量的情况下,默认使用步长1.紧跟着就是调用函数forbody来处理循环体了:

(lparser.c)
1046 static void forbody (LexState *ls, int base, int line, int nvars, int isnum) {
1047   /* forbody -> DO block */
1048   BlockCnt bl;
1049   FuncState *fs = ls->fs;
1050   int prep, endfor;
1051   adjustlocalvars(ls, 3);  /* control variables */
1052   checknext(ls, TK_DO);
1053   prep = isnum ? luaK_codeAsBx(fs, OP_FORPREP, base, NO_JUMP) : luaK_jump(fs);
1054   enterblock(fs, &bl, 0);  /* scope for declared variables */
1055   adjustlocalvars(ls, nvars);
1056   luaK_reserveregs(fs, nvars);
1057   block(ls);
1058   leaveblock(fs);  /* end of scope for declared variables */
1059   luaK_patchtohere(fs, prep);
1060   endfor = (isnum) ? luaK_codeAsBx(fs, OP_FORLOOP, base, NO_JUMP) :
1061                      luaK_codeABC(fs, OP_TFORLOOP, base, 0, nvars);
1062   luaK_fixline(fs, line);  /* pretend that `OP_FOR' starts the loop */
1063   luaK_patchlist(fs, (isnum ? endfor : luaK_jump(fs)), prep + 1);
1064 }

forbody函数的参数中,base表示循环指令相关的几个变量的开始位置,也就是指令中的A地址,nvars表示循环变量的数量,对于数值类的for循环这个变量只可能是1,对于其他类型的for循环可能有多个,参数isnum表示这个循环是否是数值类的for循环.

进入forbody函数之后,会根据nvars参数调整这个循环体的局部变量数量,其次就是根据这个for循环是不是数值类的循环来生成对应的指令,以及根据前面分析过的回填技术来回填调整地址,这里要回填的地址除了跳转指令之外,OP_FORPREP和OP_TFORLOOP指令因为也需要改变PC指针,所以也需要进行回填.

###其他循环 除了for循环之外,Lua中还有使用while以及repeat关键字实现的循环,但是另外的这几种循环,其循环条件的判断并不像for这样在for语句中进行,所以可以使用最简单的测试加跳转指令的组合来实现,换言之并没有特殊的针对这几种循环的指令,比较简单就不多做阐述.