通过字节码理解lua的for循环

SH疫情隔离在家,打算把lua和unity热更新相关的问题再升入学习一下。本篇是这个系列的第一篇。
一直以来笔者都抱有这样一个观念:lua是个好语言但被滥用了,一门胶水语言在当前的游戏开发中承担了它不该承担的任务。作为一种弱类型语言,并且没有建立足够成熟的生态(包括编辑器、第三方库)的情况下,被作为手游客户端开发的主语言,动辄几万行的业务逻辑。这对项目和开发者来说都不是一个好的选择,只能说是向ios这个傲慢的平台的一种妥协。
如果可以选择的话,我只想说:lua🐶都不写!
先看一段lua代码

local i = 0  
for i = 1,5 do  
    i = 5  
    print(i)  
end  
print("after loop")  
print(i) 

这里我们关注两个问题:

1.for循环执行了多少次?
2.afterloop之后,打印出来的i的值是多少?
loop.png

第二个问题比较好理解,由于在for循环内部出现了同名的控制变量,所以外部i实际上在循环内部没有作用域,因此并没有被赋值。
但第一个问题,为什么循环依然执行了5次,而不是在i=5这次赋值之后进入for循环的下一次判断就结束呢?
要解释这个问题,我们就需要探究一下lua代码底层到底是怎么执行的了。

lua程序的运行方式:

1.由lua解释器将lua代码翻译成指令序列

2.由lua虚拟机执行指令序列

(lua解释器和lua虚拟机均由纯粹的C语言实现,要理解更完整的实现细节可以参考lua源代码http://www.lua.org/download.html

每条lua指令由 操作码+操作数 组成

一条指令使用一个32bit的无符号整数表示,其中低6位表示操作码,操作码定义在lopcodes.h中

如何查看lua代码对应的指令序列

方法一,直接通过luac输出指令序列(这种方法没有对指令的额外解释,对新手来说不太友好)
命令行:luac -l test.lua
方法二,通过ChunkSpy.lua辅助解析
命令行:lua ChunkSpy.lua --source test.lua
(ChunkSpy.lua的获取地址https://github.com/viruscamp/luadec/tree/master/ChunkSpy

通过方法二我们可以得到上面那段代码对应的lua指令序列

1.    local i = 0  
2.003B  01000000           [01] loadk     0   0        ; R0 := K0(=0)  
3.    for i = 1,5 do  
4.003F  41400000           [02] loadk     1   1        ; R1 := K1(=1)  
5.0043  81800000           [03] loadk     2   2        ; R2 := K2(=5)  
6.0047  C1400000           [04] loadk     3   1        ; R3 := K1(=1)  
7.004B  68C00080           [05] forprep   1   4        ; R1 -= R3; pc+=4 (goto [10])  
8.        i = 5  
9.004F  01810000           [06] loadk     4   2        ; R4 := K2(=5)  
10.        print(i)  
11.0053  46C14000           [07] gettabup  5   0   259  ; R5 := U0(=_ENV)[K3(="print")]  
12.0057  80010002           [08] move      6   4        ; R6 := R4  
13.005B  64410001           [09] call      5   2   1    ;  := R5(R6)  
14.    end  
15.005F  6780FE7F           [10] forloop   1   -5       ; R1 += R3; if R1 <= R2 then { R4 := R1; pc+=-5 (goto [6]) }  
16.    print("after loop")  
17.0063  46C04000           [11] gettabup  1   0   259  ; R1 := U0(=_ENV)[K3(="print")]  
18.0067  81000100           [12] loadk     2   4        ; R2 := K4(="after loop")  
19.006B  64400001           [13] call      1   2   1    ;  := R1(R2)  
20.    print(i)  
21.006F  46C04000           [14] gettabup  1   0   259  ; R1 := U0(=_ENV)[K3(="print")]  
22.0073  80000000           [15] move      2   0        ; R2 := R0  
23.0077  64400001           [16] call      1   2   1    ;  := R1(R2)  
24.007B  26008000           [17] return    0   1        ; return  

重点关注这几行:
4-7行
9行
15行

我们解释一下其中几个关键的命令

loadk A B   --R(A) := K(B)
加载常量操作码,将B所指的常量加载到A所指的寄存器中

move A B    --R(A) := R(B)
赋值操作码,将寄存器B中的值拷贝到寄存器A中。 

forprep A B --R(A) -= R(A+2); PC += B
初始化数字for循环

forloop A B --R(A) += R(A+2); if R(A) <= R(A+1) then { R(A+3) := R(A); PC + =-B}
执行数字for循环的一次迭代

我们看一下《lua虚拟机指令简明手册》中关于for循环指令的说明:
数字for循环要求栈上的4个寄存器,每个寄存器都必须是数值。R(A)持有初始值并作为内部循环变量(内部索引);R(A+1)是界限;R(A+2)是步进值;R(A+3)是局部于 for 块的实际循环变量(外部索引)
我们注意这里,for循环用于循环移步和条件判断的始终是R(A)寄存器中的值(对应上面那段代码中的R1),而for循环内部执行段中使用的变量则是寄存器R(A+3)中的值(对应R4),并且每次重新进入循环体之前,R4还会重新被赋值为R1中的值(if R1 <= R2 then { R4 := R1} )
并且《lua虚拟机指令简明手册》特别强调了lua的for循环实现是和传统的测试+跳转的循环方式是不同的。

那么我们再来看一下所谓的“传统的测试和跳转”是怎样的执行过程。以大家最熟悉的C语言为例:

int main()  
{  
    for (int i = 0; i < 10; i++)  
    {  
        i = 10;  
    }  
  
    return 0;  
}

我们查看对应的汇编代码:

1.    for (int i = 0; i < 10; i++)  
2.007C17C8  mov         dword ptr [ebp-8],0    
3.007C17CF  jmp         main+3Ah (07C17DAh)    
4.007C17D1  mov         eax,dword ptr [ebp-8]    
5.007C17D4  add         eax,1    
6.007C17D7  mov         dword ptr [ebp-8],eax    
7.007C17DA  cmp         dword ptr [ebp-8],0Ah    
8.007C17DE  jge         main+49h (07C17E9h)    
9.    {  
10.        i = 10;  
11.007C17E0  mov         dword ptr [ebp-8],0Ah    
12.    }  
007C17E7  jmp         main+31h (07C17D1h)

注意到这里所有关于控制变量i的操作都对应于dword ptr [ebp-8]
这就是传统的力量!
看到这里,我真是要diss一下lua的for循环设计,整这些花里胡哨的特性有啥用啊?你不会比C语言之父还懂编程吧?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容