歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~程序員
做者:鄭小輝 | 騰訊 遊戲客戶端開發高級工程師
寫在前面:本文全部的文字都是我手工一個一個敲的,以及本文後面分享的Demo代碼都是我一行一行碼的,在我以前已經有很是多的前輩研究過Lua虛擬機了,因此本文不少思想必然是踏在這些巨人的肩膀上的。數據結構
本文標題是」深刻淺出Lua虛擬機」,其實重點在淺出這兩字上。畢竟做者的技術水平有限。可是據說名字要起的屌一點文章纔有人看,故而得名。函數
謹以此文奉獻給那些對Lua虛擬機有興趣的人。但願本文能達到一個拋磚引玉的效果。工具
Lua代碼的整個流程:性能
以下圖所示:程序員編碼lua文件->語法詞法分析生成Lua的字節碼文件(對應Lua工具鏈的Luac.exe)->Lua虛擬機解析字節碼,並執行其中的指令集->輸出結果。ui
藍色和綠色的部分是本文所試圖去講的內容。編碼
我不許備講Lua的全部詞法分析過程,畢竟若是浪費太多時間來寫這個的話一會策劃同窗要提刀來問我需求的開發進度如何了,因此長話短說,我就根據本身對Lua的理解,以某一個具體的例子來作分析:lua
Lua代碼塊:spa
If a < b then a = c end
這句話我們程序員能看懂,但是計算機就跟某些男程序員家裏負責貌美如花的老婆同樣,只知道這是一串用英文字符拼出來的一行沒有任何意義的字符串而已。翻譯
爲了讓計算機可以讀懂這句話,那麼咱們要作的第一件事情就是分詞:既然你看不懂。我就先把一句話拆成一個一個單詞,並且我告訴你每一個單詞的含義是什麼。
分詞的結果大概長下面這樣:
分詞結果 類型(意義) if Type_If (if 關鍵字) a Type_Var (這是一個變量) < Type_OpLess(這是一個小於號) b Type_Var(這是一個變量) then Type_Then(Then關鍵字) a Type_Var (這是一個變量) = Type_OpEqual(這是一個等號) c Type_Var(這是一個變量) end Type_End(End關鍵字)
好了。如今計算機終於明白了。原來你寫的這行代碼裏面有9個字,並且每一個字的意思我都懂了。因此如今問題是,計算機理解了這句話了嗎?
計算機依然不理解。就好像「吃飯」這句話,計算機理解了 「吃」是動詞,張開嘴巴的意思。「飯」是名詞,指的米飯的意思。可是你把吃飯放在一塊兒,計算機並不知道這是「張開嘴巴,把飯放進嘴裏,而且嚥到胃裏」的意思。由於計算機只知道「張開嘴巴」和「米飯」兩件事,這兩件事有什麼聯繫,計算機並不能理解。有人會說了:簡單:吃+其餘字 這種結構就讓計算機籠統的理解爲把後一個詞表明的東西放進嘴巴里的意思就行了啊?這種狀況適合」吃飯」這個詞,可是若是這樣你讓計算機怎麼理解「吃驚」這個詞呢?因此這裏引出下一個話題:語義解析。
關於語義解析這塊,若是你們想要了解的更深刻,能夠去了解一下AST(抽象語法樹)。然而對於咱們這個例子,咱們用簡單的方式模擬着去理解就行了。
對於Lua而言,每個關鍵字都有本身特別的結構。因此Lua的關鍵字將成爲語義解析的重點。咱們如今涉及到的if這個例子:咱們能夠簡單的用僞代碼表述這個解析過程:
對於if語句咱們能夠抽象成這種結構:
If condition(條件表達式) then dosth(語句塊) end
因此對if語句塊進行解析的僞代碼以下:
ReadTokenWord(); If(tokenWord.type == Type_If) then ReadCondition() //讀取條件表達式 ReadThen() //讀取關鍵字then ReadCodeBlock() //讀取邏輯代碼塊 ReadEnd() //讀取關鍵字End End
因此爲了讓計算機理解,咱們仍是得把這個東西變成數據結構。
由於我只是作一個Demo而已,因此我用了先驗知識。也就是我假定咱們的If語句塊邏輯結構是這樣的:
If 小於條件表達式 then 賦值表達式 End
因此在個人Demo裏轉成C++數據結構就是IfStateMent大概是這樣:
OK,因此如今,咱們整個詞法語法分析都作完了。可是真正的Lua虛擬機並不能執行咱們的ifStateMent這種東西。Lua源碼裏的實現也是相似這種TokenType 和 結構化的 if Statement whileStatement等等,而且Lua沒有生成完整的語法樹。Lua源碼的實現裏面,它是解析一些語句,生成臨時的語法樹,而後翻譯成指令集的。並不會等全部的語句都解析完了再翻譯的。語義解析和翻譯成指令集是並行的一個過程。貼一個源碼裏面關於語義解析的部分實現:
OK,如今我們已經把咱們程序員輸入的Lua代碼變成了一個數據結構(計算機能讀懂)。下一步咱們要把這個數據結構再變成Lua虛擬機能認識的東西,這個東西就是 Lua 指令集!
至於轉換的過程,對於咱們這個例子,大概是這樣的:
If a < b then a = c end
先理解條件 a<b:一種基於寄存器的指令設計大概是這樣的:
a,b均爲變量。假定咱們的可用的寄存器索引值從10(0-9號寄存器都已經被佔用了)開始:又假定咱們有一個常量索引表:0號常量:字符’a’,1號常量:字符串’b’。那麼a<b能夠被翻譯爲這樣:
同理,繼續進行a=c的翻譯等等。
因此If a < b then a = c end在我寫的demo裏面最後被翻譯成了:
OK,咱們如今大概明白了從Lua代碼怎麼變成指令集的這件事了。
如今咱們來具體看一下Lua5.1的指令集:
Lua的指令集是定長的,每一條指令都是32位,其中大概長這樣:
每一條指令的低六位 都是指令的指令碼,好比 0表明MOVE,12表明Add。Lua總共有37條指令,分別是
MOVE,LOADK,LOADBOOL,LOADNIL,GETUPVAL,GETGLOBAL,GETTABLE,SETGLOBAL,SETUPVAL,SETTABLE,NEWTABLE,SELF,ADD,SUB,MUL,DIV,MOD,POW,UNM,NOT,LEN,CONCAT,JMP,EQ,LT,LE,TEST,TESTSET,CALL,TAILCALL,RETURN,FORLOOP,TFORLOOP,SETLIST,CLOSE,CLOSURE,VARARG.
咱們發現圖上還有iABC,iABx,iAsBx。這個意思是有的指令格式是 OPCODE,A,B,C的格式,有的指令是OPCODE A,BX格式,有的是OPCODE A,sBX格式。sBx和bx的區別是bx是一個無符號整數,而sbx表示的是一個有符號的數,也就是sbx能夠是負數。
我不打算詳細的講每一條指令,我仍是舉個例子:
指令編碼 0x 00004041 這條指令怎麼解析:
0x4041 = 0000 0000 0000 0000 0100 0000 0100 0001
低六位(0~5)是opcode:000001 = 1 = LoadK指令(0~37分別對應了我上面列的38條指令,按順序來的,0是Move,1是loadk,2是loadbool.....37是vararg)。LoadK指令格式是iABC(C沒用上,僅ab有用)格式。因此咱們再繼續讀ab。
a = 低6~13位 爲 00000001 = 1因此a=1
b = 低14~22位 爲000000001 = 1因此b=1
因此0x4041 = LOADK 1, 1
指令碼如何解析我也在demo裏面寫了,代碼大概是這樣:
那麼Lua文件通過Luac的編譯後生成的Lua字節碼,Lua字節碼文件裏面除了包含指令集以外又有哪些東西呢?固然不會像我上面的那個詞法語法解析那個demo那麼弱智拉。因此下面咱們就講一下Lua字節碼文件的結構:
Lua字節碼文件(*.lua.bytes)包含了:文件頭+頂層函數:
文件頭結構:
頂層函數和其餘普通函數都擁有一樣的結構:
因此咱們是能夠輕鬆本身寫代碼去解析的。後文提供的Demo源碼裏面我也已經實現了字節碼文件的解析。
Demo中的例子是涉及到的Lua源代碼以及最終解析字節碼獲得的信息分別是:
OK,本文如今就剩最後一點點東西了:Lua虛擬機是怎麼執行這些指令的呢?
大概是這樣的:
While(指令不爲空) 執行指令 取下一條要執行的指令 End
每一條指令應該怎麼執行呢???若是你們還有印象的話,我們前文語義解析完以後轉指令集是這樣的:
a < b
那固然是指令後面的文字就已經詳細的描述了指令的執行邏輯拉,嘿嘿。
爲了真正的執行起來,因此咱們在數據結構上設計須要 1,寄存器:2,常量表:3,全局變量表:
爲了能執行咱們demo裏面的例子:
我實現了這段代碼涉及到的全部指令
insExecute[(int)OP_LOADK] = &LuaVM::LoadK; insExecute[(int)OP_SETGLOBAL] = &LuaVM::SetGlobal; insExecute[(int)OP_GETGLOBAL] = &LuaVM::GetGlobal; insExecute[(int)OP_ADD] = &LuaVM::_Add; insExecute[(int)OP_SUB] = &LuaVM::_Sub; insExecute[(int)OP_MUL] = &LuaVM::_Mul; insExecute[(int)OP_DIV] = &LuaVM::_Div; insExecute[(int)OP_CALL] = &LuaVM::_Call; insExecute[(int)OP_MOD] = &LuaVM::_Mod; insExecute[(int)OP_LT] = &LuaVM::_LT; insExecute[(int)OP_JMP] = &LuaVM::_JMP; insExecute[(int)OP_RETURN] = &LuaVM::_Return;
以Add爲例:
bool LuaVM::_Add(LuaInstrunction ins) { //R(A):=RK(B)+RK(C) ::: //Todo:必要的參數合法性檢查:若是有問題則拋異常 // 將ins.bValue表明的數據和ins.cValue表明的數據相加的結果賦值給索引值爲ins.aValue的寄存器 luaRegisters[ins.aValue].SetValue(0, GetBK(ins.bValue) + GetBK(ins.cValue)); return true; }
下面是程序的運行效果截圖:
看完整個過程,其實能夠思考這個問題:爲何Lua執行效率會遠遠低於C程序?
我的愚見:
OK,最後獻上我寫的這個demo的源代碼:這份源代碼是我在清明節在家的時候瞎寫的。也就是說代碼並無通過耐心的整理,並且清明節有人找我出去喝酒,致使我有很長一段時間都處於「我艹快點碼完我要出去喝了」這種心不在焉的狀態,因此有些編碼格式和結構設計都到處能看到隨性的例子~畢竟只是一個demo嘛。人生在世,要有佛性,隨緣就好!若是各位真的想進一步理解關於Lua虛擬機的東西,那麼我推薦諸位有空耐着性子去讀一讀Lua虛擬機的源代碼~
最後,誠摯感謝全部看到了最後這句話的同窗。謝謝大家耐着性子看完了一個技術菜雞的長篇廢話。
此文已由做者受權騰訊雲+社區發佈,須要源碼的同窗請點擊:https://cloud.tencent.com/dev...