深入理解Lua虛擬機


為了達到較高的執行效率,lua代碼並不是直接被Lua解釋器解釋執行,而是會先編譯為字節碼,然后再交給lua虛擬機去執行

lua代碼稱為chunk,編譯成的字節碼則稱為二進制chunkBinary 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源代碼如下:

1 print ("hello")
2 
3 function add(a, b)
4     return a+b
5 end

 

編譯得到的Hello.out的二進制為:

二進制chunk(Binary chunk)的格式並沒有標准化,也沒有任何官方文檔對其進行說明,一切以lua官方實現的源代碼為准。

其設計並沒有考慮跨平台,對於需要超過一個字節表示的數據,必須要考慮大小端(Endianness)問題。

lua官方實現的做法比較簡單:編譯lua腳本時,直接按照本機的大小端方式生成二進制chunk文件,當加載二進制chunk文件時,會探測被加載文件的大小端方式,如果和本機不匹配,就拒絕加載.

二進制chunk格式設計也沒有考慮不同lua版本之間的兼容問題,當加載二進制chunk文件時,會檢測其版本號,如果和當前lua版本不匹配,就拒絕加載

另外,二進制chunk格式設計也沒有被刻意設計得很緊湊。在某些情況下,一段lua代碼編譯成二進制chunk后,甚至會被文本形式的源代碼還要大。

預編譯成二進制chunk主要是為了提升加載速度,因此這也不是很大的問題.

 

頭部字段

字段 字節數 解釋
簽名(signature) byte[4] 0x1B4C7561 二進制chunk文件的魔數,分別是ESC、L、u、a的ASCII碼
版本號(version) byte 0x53

lua語言的版本號由3部分組成:大版本號(Major Version)、小版本號(Minor Version)和發布號(Release Version)

比如:當前lua的版本號為5.3.5,那么大版本號為5,小版本號為3、發布號為5

由於發布號的增加只是為了修復bug,不會對二進制chunk文件格式進行調整,因此版本號(version)只存儲了大版本號和小版本號

格式號(format) byte 0x00

二進制chunk文件的格式號

lua虛擬機在加載二進制chunk時,會檢查其格式號,如果和虛擬機本身的格式號不匹配,就會拒絕加載該文件

luacData byte[6] 0x19930D0A1A0A 其中前兩個字節0x1993,是lua1.0發布的年份;后四個字節依次是回車符(0x0D)、換行符(0x0A)、替換符(0x1A)和另一個換行符
cintSize byte 0x04 cint數據類型size
sizeSize byte 0x04 size_t數據類型size
instructionSize byte 0x04 lua虛擬機指令size
luaIntegerSize byte 0x08 lua整型size
luaNumberSize byte 0x08 lua浮點數size
lua整數值(luacInt) int64 0x7856000000000000 加載二進制chunk文件時,會用該字段檢測其大小端和本機是否匹配
格式號(luacNum) float64 0x0000000000287740

浮點數值為370.5

加載二進制chunk文件時,會用該字段檢測其使用的浮點數格式(目前主流平台一般都采用IEEE754浮點數格式)

嵌套的函數原型: 

0B4048656C6C6F2E6C7561 源文件名Hello.lua;-s去掉調試信息后,該項數值為:00
main    00000000 00000000 main函數起始行列號
00 函數參數個數
01 函數為不定參數
02 寄存器數量
06000000 函數指令數目
06004000 第1條指令
41400000 第2條指令
24400001 第3條指令
2C000000 第4條指令
08000081 第5條指令
26008000 第6條指令
03000000 常量數目
04 第1個常量tag,04表示字符串
067072696E74 第1個常量內容
04 第2個常量tag,04表示字符串
0668656C6C6F 第2個常量內容
04 第3個常量tag,04表示字符串
04616464 第3個常量內容
01000000 Upvalue數目
0100 第1個Upvalue
01000000 子函數原型數目
add   03000000 05000000 add函數起始行列號
02 函數參數個數
00 函數為不定參數
03 寄存器數量
03000000 函數指令數目
8D400000 第1條指令
A6000001 第2條指令
26008000 第3條指令
00000000 常量數目
00000000 Upvalue數目
00000000 子函數原型數目
03000000 行號數目;-s去掉調試信息后,該項數值為:00000000;為0時下面的信息也都沒有
04000000 第1條指令行號
04000000 第2條指令行號
05000000 第3條指令行號
02000000 局部變量數目;-s去掉調試信息后,該項數值為:00000000;為0時下面的信息也都沒有
0261 第1個局部變量名稱
00000000 第1個局部變量起始指令索引
03000000 第1個局部變量終止指令索引
0262 第2個局部變量名稱
00000000 第2個局部變量起始指令索引
03000000 第2個局部變量終止指令索引
00000000 Upvalue名稱數目;-s去掉調試信息后,該項數值為:00000000;為0時下面的信息也都沒有
06000000 行號數目;-s去掉調試信息后,該項數值為:00000000;為0時下面的信息也都沒有
01000000 第1條指令行號
01000000 第2條指令行號
01000000 第3條指令行號
05000000 第4條指令行號
03000000 第5條指令行號
05000000 第6條指令行號
00000000 局部變量數目;-s去掉調試信息后,該項數值為:00000000;為0時下面的信息也都沒有
01000000 Upvalue名稱數目;-s去掉調試信息后,該項數值為:00000000;為0時下面的信息也都沒有
055F454E56 第1個Upvalue名稱

注1:二進制chunk中的字符串分為三種情況:

①NULL字符串用0x00表示 

②長度小於等於253(0xFD)的字符串,先用1個byte存儲字符串長度+1的數值,然后是字節數組 

③長度大於等於254(0xFE)的字符串,第一個字節是0xFF,后面跟一個8字節size_t類型存儲字符串長度+1的數值,然后是字節數組 

注2:常量tag對應表

tag lua字面量類型 存儲類型
0x00 nil 不存儲
0x01 boolean 字節(0、1)
0x03 number lua浮點數
0x13 integer lua整數
0x04 string 短字符串
0x14 string 長字符串

 

查看二進制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中

所有的指令執行,都是基於一個操作數棧的。你想要執行任何指令時,對不起,得先入棧,然后算完了再給我出棧。

總的來說,就是抽象出了一個高度可移植的操作數棧,所有代碼都會被編譯成字節碼,然后字節碼就是在玩這個棧。 好處是實現簡單,移植性強。

壞處是指令條數比較多,數據轉移次數比較多,因為每一次入棧出棧都牽涉數據的轉移。

 

基於寄存器的虛擬機,會轉化成如下指令

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

類型 示例 說明
OpArgN

B:1 C A:3 MOVE

不表示任何信息,不會被使用

MOVE指令的C操作數為OpArgN類型

OpArgR

B:1 C A:3 MOVE

B:2 C:4 A:1 CONCAT

iABC指令中的B、C操作數,表示寄存器索引
sBx:-3 A:0 FORLOOP iAsBx指令中的sBx操作數,表示跳轉偏移
OpArgK

Bx:2 A:4 LOADK

LOADK指令中的Bx操作數,表示常量表索引
B:0x001 C:0x100 A:4 ADD

iABC指令中的B、C操作數

B、C操作數只能使用9bits中的低8位

最高位為1,表示常量表索引

最高位為0,表示寄存器索引

OpArgU

B:0 C:1 A:2 LOADBOOL

表示布爾值、整數值、Upvalue索引、子函數索引等

 

Lua棧索引

 

注1:絕對索引是從1開始由棧底到棧頂依次增長的

注2:相對索引是從-1開始由棧頂到棧底依次遞減的(在lua API函數內部會將相對索引轉換為絕對索引)

注3:上圖棧的容量為7,棧頂絕對索引為5,有效索引范圍為:[1, 5],可接受索引范圍為:[1, 7]

注4:Lua虛擬機指令里寄存器索引是從0開始的,而Lua API里的棧索引是從1開始的,因此當需要把寄存器索引當成棧索引使用時,要進行+1

 

Lua State

 

指令表

指令名稱 類型 操作碼

操作數類型

(B|C|A、Bx|A、sBx|A、Ax)

示例說明 公式
MOVE iABC 0x00 OpArgR OpArgN 目標寄存器idx

B:1 C A:3 MOVE 

把源寄存器(索引由B指定)里的值移動到目標寄存器(索引有A指定)

常用於局部變量賦值和參數傳遞

R(A) := R(B) 
LOADK iABx 0x01 OpArgK 目標寄存器idx

Bx:2 A:4 LOADK

給單個寄存器(索引由A指定)設置成常量(其在常量表的索引由Bx指定)

將常量表里的某個常量加載到指定寄存器

在lua中,數值型、字符串型等局部變量賦初始值 (數字和字符串會放到常量表中)

R(A) := Kst(Bx) 
LOADKX iABx 0x02 OpArgN 目標寄存器idx

Bx A:4 LOADKX

Ax:585028 EXTRAARG

LOADK使用Bx(18bits,最大無符號整數為262143)表示常量表索引

當將lua作數據描述語言使用時,常量表可能會超過這個限制,

為了應對這種情況,lua提供了LOADKX指令

LOADKX指令需要和EXTRAAG指令搭配使用,用后者的Ax(26bits)操作數來指定常量索引

R(A) := Kst(Ax)  
LOADBOOL iABC 0x03 OpArgU OpArgU 目標寄存器idx

B:0 C:1 A:2 LOADBOOL

給單個寄存器(索引由A指定)設置布爾值(布爾值由B指定)

如果寄存器C為非0則跳過下一條指令

 
R(A) := (bool)B  if(C) pc++ 
LOADNIL iABC 0x04 OpArgU OpArgN 目標寄存器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

GETUPVAL iABC 0x05 OpArgU OpArgN 目標寄存器idx

B:1 C A:3 GETUPVAL

把當前閉包的某個Upvalue值(索引由B指定)拷貝到目標寄存器(索引由A指定)中 

 

R(A) := Upvalue[B] 
GETTABUP iABC 0x06 OpArgU OpArgK 目標寄存器idx

B:0 C:0x002 A:3 GETTABUP

把當前閉包的某個Upvalue值(索引由B指定)拷貝到目標寄存器(索引由A指定)中

與GETUPVAL不同的是,Upvalue從表里取值(鍵由C指定,為寄存器或常量表索引)

 

R(A) := Upvalue[B][RK(C)]
GETTABLE iABC 0x07 OpArgR OpArgK 目標寄存器idx

B:0 C:0x002 A:3 GETTABLE

把表中某個值拷貝到目標寄存器(索引由A指定)中

表所在寄存器索引由B指定,鍵由C(為寄存器或常量表索引)指定

 
R(A) := R[B][RK(C)]
SETTABUP iABC 0x08 OpArgK OpArgK 目標寄存器idx

B:0x002 C:0x003 A:0 SETTABUP

設置當前閉包的某個Upvalue值(索引由A指定)為寄存器或常量表的某個值(索引由C指定)

與SETUPVAL不同的是,Upvalue從表里取值(鍵由B指定,為寄存器或常量表索引)

 

Upvalue[A][RK(B)] := RK(C)
SETUPVAL iABC 0x09 OpArgU OpArgN 目標寄存器idx

B:0 C A:3 SETUPVAL

設置當前閉包的某個Upvalue值(索引由B指定)為寄存器的某個值(索引由A指定)

Upvalue[B] := R(A)
SETTABLE iABC 0x0A OpArgK OpArgK 目標寄存器idx 

B:0x002 C:0x003 A:1 SETTABLE

給寄存器中的表(索引由A指定)的某個鍵進行賦值

鍵和值分別由B和C指定(為寄存器或常量表索引)

R(A)[RK(B)] := RK(C)
NEWTABLE iABC 0x0B OpArgU OpArgU 目標寄存器idx 

B:0 C:2 A:4 NEWTABLE

創建空表,並將其放入指定寄存器(索引有A指定)

表的初始數組容量和哈希表容量分別有B和C指定

R(A) := {} (size = B, C)
SELF iABC 0x0C OpArgR OpArgK 目標寄存器idx 

B:1 C:0x100 A:2 SELF

把寄存器中對象(索引由B指定)和常量表中方法(索引由C指定)拷貝到相鄰的兩個目標寄存器中

起始目標寄存器的索引由A指定

R(A+1) := R(B)

R(A) := R(B)[RK(C)]

ADD(+,加) iABC 0x0D OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 ADD

對兩個寄存器或常量值(索引由B和C指定)進行相加,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) + RK(C)

SUB

(-,減)

iABC 0x0E OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 SUB

對兩個寄存器或常量值(索引由B和C指定)進行相減,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) - RK(C)

MUL

(*,乘)

iABC 0x0F OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 MUL

對兩個寄存器或常量值(索引由B和C指定)進行相乘,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) * RK(C)

MOD

(%,求模)

iABC 0x10 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 MOD

對兩個寄存器或常量值(索引由B和C指定)進行求摸運算,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) % RK(C)

POW

(^,求冪)

iABC 0x11 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 POW

對兩個寄存器或常量值(索引由B和C指定)進行求冪運算,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) ^ RK(C)

DIV

(/,除)

iABC 0x12 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 DIV

對兩個寄存器或常量值(索引由B和C指定)進行相除,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) / RK(C)

IDIV

(//,整除)

iABC 0x13 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 IDIV

對兩個寄存器或常量值(索引由B和C指定)進行相整除,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) // RK(C)

BAND

(&,與)

iABC 0x14 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 BAND

對兩個寄存器或常量值(索引由B和C指定)進行求與操作,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) & RK(C)

BOR

(|,或)

iABC 0x15 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 BOR

對兩個寄存器或常量值(索引由B和C指定)進行求或操作,並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) | RK(C)

BXOR

(~,異或)

iABC 0x16 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 BXOR

對兩個寄存器或常量值(索引由B和C指定)進行求異或操作,並將結果放入另一個寄存器中(索引由A指定)

 

R(A) := RK(B) ~ RK(C)

SHL

(<<,左移)

iABC 0x17 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 SHL

索引由B指定的寄存器或常量值進行左移位操作(移動位數的索引由C指定的寄存器或常量值)

並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) << RK(C)

SHR

(>>,右移)

iABC 0x18 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:4 SHR

索引由B指定的寄存器或常量值進行右移位操作(移動位數的索引由C指定的寄存器或常量值)

並將結果放入另一個寄存器中(索引由A指定)

R(A) := RK(B) >> RK(C)

UNM

(-,取負數)

iABC 0x19 OpArgR OpArgN 目標寄存器idx 

B:1 C A:3 UNM

對寄存器(索引由B指定)進行取負數操作,並將結果放入另一個寄存器中(索引由A指定)

R(A) := - R(B)

BNOT

(~,取反)

iABC 0x1A OpArgR OpArgN 目標寄存器idx 

B:1 C A:3 BNOT

對寄存器(索引由B指定)進行取反操作,並將結果放入另一個寄存器中(索引由A指定)

R(A) := ~ R(B)
NOT iABC 0x1B OpArgR OpArgN 目標寄存器idx 

B:1 C A:3 NOT

對寄存器(索引由B指定)進行求非操作,並將結果放入另一個寄存器中(索引由A指定)

R(A) := not R(B)
LEN iABC 0x1C OpArgR OpArgN 目標寄存器idx 

B:1 C A:3 LEN

對寄存器(索引由B指定)進行求長度操作,並將結果放入另一個寄存器中(索引由A指定)

R(A) := length of R(B)
CONCAT iABC 0x1D OpArgR OpArgR 目標寄存器idx 

B:2 C:4 A:1 CONCAT

將連續n個寄存器(起始索引和終止索引由B和C指定)里的值進行拼接

並將結果放入另一個寄存器中(索引由A指定)

R(A) := R(B) .. ... .. R(C)
JMP iAsBx 0x1E OpArgR 目標寄存器idx 

sBx:-1 A JMP

當sBx不為0時,進行無條件跳轉

執行pc = pc + sBx(sBx為-1,表示將當前指令再執行一次  注:這將是一個死循環)

sBx:0 A:0x001 JMP

當sBx為0時(繼續執行后面指令,不跳轉)

用於閉合處於開啟狀態的Upvalue(即:把即將銷毀的局部變量的值復制出來,並更新到某個Upvalue中)

當前閉包的某個Upvalue值的索引由A指定

 
EQ(==) iABC 0x1F OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:1 EQ

寄存器或常量表(索引由B指定)是否等於寄存器或常量表(索引由C指定)

若結果等於操作數A,則跳過下一條指令

if ((RK(B) == RK(C)) pc++
LT(<) iABC 0x20 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:1 LT

寄存器或常量表(索引由B指定)是否小於寄存器或常量表(索引由C指定)

若結果等於操作數A,則跳過下一條指令

if ((RK(B) < RK(C)) pc++ 
LE(<=) iABC 0x21 OpArgK OpArgK 目標寄存器idx 

B:0x001 C:0x100 A:1 LE

寄存器或常量表(索引由B指定)是否小於等於寄存器或常量表(索引由C指定)

若結果等於操作數A,則跳過下一條指令

if ((RK(B) <= RK(C)) pc++
TEST iABC 0x22 OpArgN OpArgU 目標寄存器idx 

B C:0 A:1 TEST

判斷寄存器(索引由A指定)中的值轉換為bool值后,是否和操作數C表示的bool值一致

若結果不一致,則跳過下一條指令

if not (R(A) <=> C) pc++ 

注:<=>表示按bool值比較

TESTSET iABC 0x23 OpArgR OpArgU 目標寄存器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值比較

CALL

iABC 0x24 OpArgU OpArgU 目標寄存器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), ... ,
TAILCALL iABC 0x25 OpArgU OpArgU 目標寄存器idx 

函數調用一般通過調用棧來實現。用這種方法,每調用一個函數都會產生一個調用幀。

如果調用層次太深(如遞歸),容易導致棧溢出。尾遞歸優化則可以讓我們發揮遞歸函數調用威力的同時,避免調用棧溢出。

利用這種優化,被調函數可以重用主調函數的調用幀,因此可有效緩解調用棧溢出症狀。不過該優化只適合某些特定情況。

如:return f(args) 會被編譯器優化成TAILCALL指令

return R(A)(R(A+1), ... , R(A+B-1))
RETURN iABC 0x26 OpArgU OpArgN 目標寄存器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)

FORLOOP iAsBx 0x27 OpArgR 目標寄存器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)

注:當步長為正數時<?=為<=

      當步長為負數時<?=為>=

FORPREP iAsBx 0x28 OpArgR 目標寄存器idx 

數值for循環:用於按一定步長遍歷某個范圍內的數值  如:for i=1,100,2  do  f()  end // 初始值為1,步長為2,上限為100

 

該指令的目的是在循環之前預先將i減去步長(得到-1),然后跳轉到FORLOOP指令正式開始循環 

R(A)-=R(A+2)

pc+=sBx

TFORCALL iABC 0x29 OpArgN OpArgU 目標寄存器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))
TFORLOOP iAsBx 0x2A OpArgR 目標寄存器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

SETLIST iABC 0x2B OpArgU OpArgU 目標寄存器idx 

SETTABLE是通用指令,每次只處理一個鍵值對,具體操作交給表去處理,並不關心實際寫入的是表的hash部分還是數組部分。

SETLIST則是專門給數組准備的,用於按索引批量設置數組元素。其中數組位於寄存器中,索引由操作數A指定;

需要寫入數組的一系列值也在寄存器中,緊挨着數組,數量由操作數B指定;數組起始索引則由操作數C指定。

 

因為C操作數只有9bits,所以直接用它表示數組索引顯然不夠用。

這里解決辦法是讓C操作數保存批次數,然后用批次數乘上批大小(FPF,默認為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

CLOSURE iABx 0x2C OpArgU OpArgN 目標寄存器idx 

把當前Lua函數的子函數原型實例化為閉包,放入由操作數A指定的寄存器中

子函數原型來自於當前函數原型的子函數原型表,索引由操作數Bx指定

 

下圖為將prototypes表中索引為1的g子函數,放入索引為4的寄存器中

R(A) := closure(KPROTO[Bx])
VARARG iABC 0x2D OpArgU OpArgN 目標寄存器idx 

把傳遞給當前函數的變長參數加載到連續多個寄存器中。

其中第一個寄存器的索引由操作數A指定,寄存器數量由操作數B指定,操作數C沒有使用

操作數B若大於1,表示把B-1個vararg參數復制到寄存器中,否則只能等於0

R(A),R(A+1),...R(A+B-2)=vararg
EXTRAARG iAx 0x2E OpArgU

Ax:67108864 EXTRAARG

Ax有26bits,用來指定常量索引,可存放最大無符號整數為67108864,可滿足大部分情況的需要了

 

 

參考

《自己動手實現Lua》源代碼

Lua設計與實現--虛擬機篇

Lua 5.3 Bytecode Reference 

Lua 源碼解析 

Lua虛擬機棧結構及相關數據結構 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM