本文從一個簡單示例入手,詳細講解 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)。
原型結構使用這種嵌套遞歸結構,來描述函數中定義的子函數:

注:lua 允許開發者可將語句寫到文件的全局范圍中,這是因為 lua 在編譯時會將整個文件放到一個稱之為 main 函數中,并以它為起點進行編譯。
Hello.lua 源代碼如下:
print ("hello")
function add(a, b)
return a+b
end
編譯得到的 Hello.out 的二進制為:

二進制 chunk(Binary chunk)的格式并沒有標準化,也沒有任何官方文檔對其進行說明,一切以 lua 官方實現的源代碼為準。其設計并沒有考慮跨平臺,對于需要超過一個字節表示的數據,必須要考慮大小端(Endianness)問題。
lua 官方實現的做法比較簡單:編譯 lua 腳本時,直接按照本機的大小端方式生成二進制 chunk 文件,當加載二進制 chunk 文件時,會探測被加載文件的大小端方式,如果和本機不匹配,就拒絕加載。二進制 chunk 格式設計也沒有考慮不同 lua 版本之間的兼容問題,當加載二進制 chunk 文件時,會檢測其版本號,如果和當前 lua 版本不匹配,就拒絕加載。另外,二進制 chunk 格式設計也沒有被刻意設計得很緊湊。在某些情況下,一段 lua 代碼編譯成二進制 chunk 后,甚至會被文本形式的源代碼還要大。預編譯成二進制 chunk 主要是為了提升加載速度,因此這也不是很大的問題。
頭部字段:

嵌套的函數原型:

注 1:二進制 chunk 中的字符串分為三種情況:
①NULL 字符串用 0x00 表示;
② 長度小于等于 253(0xFD)的字符串,先用 1 個 byte 存儲字符串長度+1 的數值,然后是字節數組;
③ 長度大于等于 254(0xFE)的字符串,第一個字節是 0xFF,后面跟一個 8 字節 size_t 類型存儲字符串長度+1 的數值,然后是字節數組。
注 2:常量 tag 對應表

查看二進制 chunk 中的所有函數(精簡模式):
luac.exe -l Hello.lua
luac.exe -l Hello.out

注 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 <Test2.lua:0,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 0
function <Test2.lua:3,5> (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:
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的值壓入stack
push c; // 將變量c的值壓入stack
add; // 將stack頂部的兩個值彈出后相加,然后將結果壓入stack頂
mov a; // 將stack頂部結果放到a中
所有的指令執行,都是基于一個操作數棧的。你想要執行任何指令時,對不起,得先入棧,然后算完了再給我出棧??偟膩碚f,就是抽象出了一個高度可移植的操作數棧,所有代碼都會被編譯成字節碼,然后字節碼就是在玩這個棧。好處是實現簡單,移植性強。壞處是指令條數比較多,數據轉移次數比較多,因為每一次入棧出棧都牽涉數據的轉移。
基于寄存器的虛擬機,會轉化成如下指令:
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)

4 種模式中,只有 iAsBx 下的 sBx 操作數會被解釋成有符號整數,其他情況下操作數均被解釋為無符號整數。操作數 A 主要用來表示目標寄存器索引,其他操作數按表示信息可分為 4 種類型:OpArgN、OpArgU、OpArgR、OpArgK:

Lua 棧索引

注 1:絕對索引是從 1 開始由棧底到棧頂依次增長的;
注 2:相對索引是從-1 開始由棧頂到棧底依次遞減的(在 lua API 函數內部會將相對索引轉換為絕對索引);
注 3:上圖棧的容量為 7,棧頂絕對索引為 5,有效索引范圍為:[1,5],可接受索引范圍為:[1, 7];
注 4:Lua 虛擬機指令里寄存器索引是從 0 開始的,而 Lua API 里的棧索引是從 1 開始的,因此當需要把寄存器索引當成棧索引使用時,要進行+1。
Lua State

指令表
下面是 Lua 的 47 條指令詳細說明:

B:1 C A:3 MOVE
把源寄存器(索引由 B 指定)里的值移動到目標寄存器(索引有 A 指定),常用于局部變量賦值和參數傳遞。

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

Bx:2 A:4 LOADK
給單個寄存器(索引由 A 指定)設置成常量(其在常量表的索引由 Bx 指定),將常量表里的某個常量加載到指定寄存器。
在 lua 中,數值型、字符串型等局部變量賦初始值 (數字和字符串會放到常量表中):

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

Bx A:4 LOADKX
Ax:585028 EXTRAARG
LOADK 使用 Bx(18bits,最大無符號整數為 262143)表示常量表索引。當將 lua 作數據描述語言使用時,常量表可能會超過這個限制,為了應對這種情況,lua 提供了 LOADKX 指令。LOADKX 指令需要和 EXTRAAG 指令搭配使用,用后者的 Ax(26bits)操作數來指定常量索引。
公式:R(A) := Kst(Ax)
指令名稱類型操作碼BCA
LOADBOOLiABC0x03OpArgUOpArgU目標寄存器 idx
B:0 C:1 A:2 LOADBOOL
給單個寄存器(索引由 A 指定)設置布爾值(布爾值由 B 指定),如果寄存器 C 為非 0 則跳過下一條指令。

公式:
R(A) := (bool)B
if(C) pc++
指令名稱類型操作碼BCA
LOADNILiABC0x04OpArgUOpArgN目標寄存器 idx
B:4 C A:0 LOADNIL
將序號[A,A+B]連續 B+1 個寄存器設置成 nil 值,用于給連續 n 個寄存器放置 nil 值。在 lua 中,局部變量的默認初始值為 nil,LOADNIL 指令常用于給連續 n 個局部變量設置初始值。

公式:R(A), R(A+1), ... ,R(A+B) := nil
指令名稱類型操作碼BCA
GETUPVALiABC0x05OpArgUOpArgN目標寄存器 idx
B:1 C A:3 GETUPVAL
把當前閉包的某個 Upvalue 值(索引由 B 指定)拷貝到目標寄存器(索引由 A 指定)中 。

公式:R(A) := Upvalue[B]
指令名稱類型操作碼BCA
GETTABUPiABC0x06OpArgUOpArgK目標寄存器 idx
B:0 C:0x002 A:3 GETTABUP
把當前閉包的某個 Upvalue 值(索引由 B 指定)拷貝到目標寄存器(索引由 A 指定)中,與 GETUPVAL 不同的是,Upvalue 從表里取值(鍵由 C 指定,為寄存器或常量表索引)。

R(A) := Upvalue[B][rk(c)]
指令名稱類型操作碼BCA
GETTABLEiABC0x07OpArgROpArgK目標寄存器 idx
B:0 C:0x002 A:3 GETTABLE
把表中某個值拷貝到目標寄存器(索引由 A 指定)中,表所在寄存器索引由 B 指定,鍵由 C(為寄存器或常量表索引)指定。

公式:R(A) := R[B][rk(c)]
指令名稱類型操作碼BCA
SETTABUPiABC0x08OpArgKOpArgK目標寄存器 idx
B:0x002 C:0x003 A:0 SETTABUP
設置當前閉包的某個 Upvalue 值(索引由 A 指定)為寄存器或常量表的某個值(索引由 C 指定),與 SETUPVAL 不同的是,Upvalue 從表里取值(鍵由 B 指定,為寄存器或常量表索引)。

Upvalue[A][rk(b)] := RK(C)
指令名稱類型操作碼BCA
SETUPVALiABC0x09OpArgUOpArgN目標寄存器 idx
B:0 C A:3 SETUPVAL
設置當前閉包的某個 Upvalue 值(索引由 B 指定)為寄存器的某個值(索引由 A 指定)。

公式:Upvalue[B] := R(A)
指令名稱類型操作碼BCA
SETTABLEiABC0x0AOpArgKOpArgK目標寄存器 idx
B:0x002 C:0x003 A:1 SETTABLE
給寄存器中的表(索引由 A 指定)的某個鍵進行賦值,鍵和值分別由 B 和 C 指定(為寄存器或常量表索引)。

公式:R(A)[RK(B)] := RK(C)
指令名稱類型操作碼BCA
NEWTABLEiABC0x0BOpArgUOpArgU目標寄存器 idx
B:0 C:2 A:4 NEWTABLE
創建空表,并將其放入指定寄存器(索引有 A 指定),表的初始數組容量和哈希表容量分別有 B 和 C 指定。

公式:R(A) := {} (size = B, C)
指令名稱類型操作碼BCASELFiABC0x0COpArgROpArgK目標寄存器 idx
B:1 C:0x100 A:2 SELF
把寄存器中對象(索引由 B 指定)和常量表中方法(索引由 C 指定)拷貝到相鄰的兩個目標寄存器中,起始目標寄存器的索引由 A 指定。

公式:
R(A+1) := R(B)
R(A) := R(B)[RK(C)]
指令名稱類型操作碼BCAADDiABC0x0DOpArgKOpArgK目標寄存器 idx
B:0x001 C:0x100 A:4 ADD
對兩個寄存器或常量值(索引由 B 和 C 指定)進行相加,并將結果放入另一個寄存器中(索引由 A 指定)。

公式: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 指定)。

公式:R(A) := ~ R(B)
指令名稱類型操作碼BCANOTiABC0x1BOpArgROpArgN目標寄存器 idx
B:1 C A:3 NOT
對寄存器(索引由 B 指定)進行求非操作,并將結果放入另一個寄存器中(索引由 A 指定)。

公式:R(A) := not R(B)
指令名稱類型操作碼BCALENiABC0x1COpArgROpArgN目標寄存器 idx
B:1 C A:3 LEN
對寄存器(索引由 B 指定)進行求長度操作,并將結果放入另一個寄存器中(索引由 A 指定)。

公式:R(A) := length of R(B)
指令名稱類型操作碼BCA
CONCATiABC0x1DOpArgROpArgR目標寄存器 idx
B:2 C:4 A:1 CONCAT
將連續 n 個寄存器(起始索引和終止索引由 B 和 C 指定)里的值進行拼接,并將結果放入另一個寄存器中(索引由 A 指定)。

公式: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,則跳過下一條指令。

公式: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 值一致,若結果不一致,則跳過下一條指令。

公式:
if not (R(A) <=> C) pc++
注:<=>表示按 bool 值比較
指令名稱類型操作碼BCA
TESTSETiABC0x23OpArgROpArgU目標寄存器 idx
B:3 C:0 A:1 TESTSET
判斷寄存器(索引由 B 指定)中的值轉換為 bool 值后,是否和操作數 C 表示的 bool 值一致,若結果一致,將寄存器(索引由 B 指定)中的值復制到寄存器中(索引由 A 指定),否則跳過下一條指令。

公式:
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

公式:R(A), ... ,
指令名稱類型操作碼BCA
TAILCALLiABC0x25OpArgUOpArgU目標寄存器 idx
函數調用一般通過調用棧來實現。用這種方法,每調用一個函數都會產生一個調用幀。
如果調用層次太深(如遞歸),容易導致棧溢出。尾遞歸優化則可以讓我們發揮遞歸函數調用威力的同時,避免調用棧溢出。利用這種優化,被調函數可以重用主調函數的調用幀,因此可有效緩解調用棧溢出癥狀。不過該優化只適合某些特定情況。
如:return f(args) 會被編譯器優化成 TAILCALL 指令,公式:return R(A)(R(A+1), ... , R(A+B-1))
指令名稱類型操作碼BCA
RETURNiABC0x26OpArgUOpArgN目標寄存器 idx
B:4 C A:2 RETURN
把存放在連續多個寄存器里的值返回給父函數,其中第一個寄存器的索引由操作數 A 指定,寄存器數量由操作數 B 指定,操作數 C 沒有使用,需要將返回值推入棧頂:
① B==1,不需要返回任何值
② B > 1,需要返回 B-1 個值;這些值已經在寄存器中了,只用再將它們復制到棧頂即可
③ B==0,一部分返回值已經在棧頂了,只需將另一部分也推入棧頂即可

公式:return R(A),...,R(A+B-2)
指令名稱類型操作碼sBxAFORLOOPiAsBx0x27OpArgR目標寄存器 idx
數值 for 循環:用于按一定步長遍歷某個范圍內的數值 如:for i=1,100,2 do f() end // 初始值為 1,步長為 2,上限為 100
該指令先給 i 加上步長,然后判斷 i 是否在范圍之內。若已經超出范圍,則循環結束;若為超出范圍,則將數值拷貝給用戶定義的局部變量,然后跳轉到循環體內部開始執行具體的代碼塊。

公式:
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 指令正式開始循環:

公式:
R(A)-=R(A+2)
pc+=sBx
指令名稱類型操作碼BCA
TFORCALLiABC0x29OpArgNOpArgU目標寄存器 idx
通用 for 循環:for k,v in pairs(t) do print(k,v) end
編譯器使用的第一個特殊變量(generator):f 存放的是迭代器,其他兩個特殊變量(state):s、(control):var 來調用迭代器,把結果保存在用戶定義的變量 k、v 中。

公式: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,則循環結束。

公式:
if R(A+1) ~= nil
R(A)=R(A+1)
pc+=sBx
指令名稱類型操作碼BCA
SETLISTiABC0x2BOpArgUOpArgU目標寄存器 idx
SETTABLE 是通用指令,每次只處理一個鍵值對,具體操作交給表去處理,并不關心實際寫入的是表的 hash 部分還是數組部分。SETLIST 則是專門給數組準備的,用于按索引批量設置數組元素。其中數組位于寄存器中,索引由操作數 A 指定;需要寫入數組的一系列值也在寄存器中,緊挨著數組,數量由操作數 B 指定;數組起始索引則由操作數 C 指定。
因為 C 操作數只有 9bits,所以直接用它表示數組索引顯然不夠用。這里解決辦法是讓 C 操作數保存批次數,然后用批次數乘上批大?。‵PF,默認為 50)就可以算出數組的起始索引。因此,C 操作數能表示的最大索引為 25600(50*512),當數組長度大于 25600 時,SETLIST 指令后會跟一條 EXTRAARG 指令,用其 Ax 操作數來保存批次數。
綜上,C>0,表示的是批次數+1,否則,真正批次數存放在后續的 EXTRAARG 指令中。
操作數 B 為 0 時,當表構造器的最后一個元素是函數調用或者 vararg 表達式時,Lua 會把它們產生的所有值都收集起來供 SETLIST 使用。

公式:
R(A)[(C-1)*FPF+i] := R(A+i)
1 <= i <= B
指令名稱類型操作碼BxACLOSUREiABx0x2COpArgU目標寄存器 idx
把當前 Lua 函數的子函數原型實例化為閉包,放入由操作數 A 指定的寄存器中子函數原型來自于當前函數原型的子函數原型表,索引由操作數 Bx 指定。
下圖為將 prototypes 表中索引為 1 的 g 子函數,放入索引為 4 的寄存器中:

公式:R(A) := closure(KPROTO[Bx])
指令名稱類型操作碼BCA
VARARGiABC0x2DOpArgUOpArgN目標寄存器 idx
把傳遞給當前函數的變長參數加載到連續多個寄存器中。
其中第一個寄存器的索引由操作數 A 指定,寄存器數量由操作數 B 指定,操作數 C 沒有使用,操作數 B 若大于 1,表示把 B-1 個 vararg 參數復制到寄存器中,否則只能等于 0。

公式:R(A),R(A+1),...R(A+B-2)=vararg
指令名稱類型操作碼AxEXTRAARGiAx0x2EOpArgU
Ax:67108864 EXTRAARG
Ax 有 26bits,用來指定常量索引,可存放最大無符號整數為 67108864,可滿足大部分情況的需要了。