Lua2.4 保存字節碼 dump.c

嚴格意義上說,把 dump 這部分叫保存字節碼並不許確。
由於除了保存 TFunc 裏的字節碼 code 以外,還保存了其它的內容。好比函數頭,字節序及字節碼須要的數據等。因此,準確的說應該叫保存字節碼及環境,或者叫作保存世界,就是字節碼生成以後的運行時相關信息也保存了下來。
能夠從保存下來的這些信息恢復出字節碼執行時須要的運行時,默認的保存文件就是以前所說的那個 luac.out 的二進制文件。
咬文嚼字一下,dump 這裏翻譯爲保存意思應該差很少,undump 則是它的相反操做,能夠叫作恢復。dump 這個詞在程序員使用時其實不翻譯意思也是比較明確的。好比,常見的就是 coredump 或者 dump 文件之類的。因此,下面說的過程當中在可能再也不區別保存或者 dump 了。
進入正題。
luac.c 裏調了兩個和 dump 相關的函數: DumpHeader 和 DumpFunction。下面分別看一下:

程序員

void DumpHeader(FILE* D)
{
 Word w=TEST_WORD;
 float f=TEST_FLOAT;
 fputc(ID_CHUNK,D);
 fputs(SIGNATURE,D);
 fputc(VERSION,D);
 fwrite(&w,sizeof(w),1,D);
 fwrite(&f,sizeof(f),1,D);
}

函數是保存一些全局性的環境信息 。
保存 ID_CHUNK,至關於一個標籤,這樣在解釋器直接執行編譯器生成的二進制文件 luac.out 時,看到文件的第一個字節是 ID_CHUNK 它就知道這是一個編譯過的文件,就再也不進行編譯,而是調用相應的 undump 從 luac.out 中得到所保存的所有信息。
接着保存字符串 "Lua" 和版本號,版本號是一個十六進制數 0x23,這應該用的是和 Lua2.3 同樣的格式。
保存一個字和一個浮點值,這兩個是爲了肯定字節序(在不一樣的處理器體系結構上,字節序可能不一樣,就是常說的大端序或者小端序,網上有介紹,不深刻了。不過一下子看代碼的話也是能看出來的,由於代碼裏在對字節序不一樣時有交換操做)。

在看 DumpFunction 以前,先看下 ThreadCode 。
ThreadCode 主要作了一件事,把符號和字符串位置信息在字節碼中清空,把相同的符號或字符串位置連起來。這可能也是這個函數叫 ThreadCode 的緣由。
程序一上來先把符號和字符串的下標清 0,就是頭兩個 for 循環作的事。
在第三個 for 循環裏,簡單解析字節碼指令,經過 SawVar 和 SawStr 設置符號或字符串在哪裏(字節碼相於字節碼起始處的偏移量)被使用。
舉個小例子,好比 SawVar 第一次被調用時,at 是字節碼的偏移量,c.w 是索引,返回 0。
以一樣的 c.w 第二次調用 SawVar 時,返回的就是上一次的 at。這樣就至關於把相同的符號引用的地方串連起來了。
這麼作的另外一個緣由是沒有用到的符號或者字符串不會被 dump ,節省了空間。參見 DumpStrings 方法裏保存以前先判斷相應的索引是否爲 0。
這麼說可能有點抽象,稍後看一個小例子就什麼都明白了。而 undump.c 裏的 Unthread 函數就是上面這個串連過程的逆過程。
函數

void DumpFunction(TFunc* tf, FILE* D)
{
 lastF=tf;
 ThreadCode(tf->code,tf->code+tf->size);
 fputc(ID_FUN,D);
 DumpSize(tf->size,D);
 DumpWord(tf->lineDefined,D);
 if (IsMain(tf))
  DumpString(tf->fileName,D);
 else
  DumpWord(tf->marked,D);
 DumpBlock(tf->code,tf->size,D);
 DumpStrings(D);
}

保存函數相關信息。
串連符號和字符串,字節碼裏面作出相應的改動。
dump ID_FUN 標籤,就是字符 'F'。
dump 函數的大小 size。
dump 函數的定義行號 lineDefined。
若是是主函數,dump 文件名。
若是不是主函數,dump 它的標記 marked。
dump 字節碼。
dump 字符串信息,包括符號和字符串。

其它的方法比較直觀,不細看了,下面看例子的時候能夠很直觀的看到。
單說下 DumpStrings。
lua

static void DumpStrings(FILE* D)
{
 int i;
 for (i=0; i<lua_ntable; i++)
 {
  if (VarLoc(i)!=0)
  {
   fputc(ID_VAR,D);
   DumpWord(VarLoc(i),D);
   DumpString(VarStr(i),D);
  }
  VarLoc(i)=i;
 }
 for (i=0; i<lua_nconstant; i++)
 {
  if (StrLoc(i)!=0)
  {
   fputc(ID_STR,D);
   DumpWord(StrLoc(i),D);
   DumpString(StrStr(i),D);
  }
  StrLoc(i)=i;
 }
}

看一下上面的那個 for 循環,它是用來保存符號的。
循環裏面,先判斷符號的索引是否爲 0 ,若是不是 0 ,說明符號在函數中被使用了,就保存它。
保存的時候,先 dump 一個 ID_VAR 標籤,也就是 'V',代表下面保存的是一個符號。
接着保存它在字節碼中最後一次出現的位置,這個位置就是上面 ThreadCode 中調整過的那個位置。
保存符號的字符串。(注意,保存字符串時保存的有字符串的長度和字符串自己。)
恢復符號在索引值,以備下一次 DumpFunction 使用。

下面看一個小例子,以加深理解。
---------------------------------
翻譯

function add(x, y)
    return x + y
end
print (add(3, 4))

---------------------------------
這個仍是以前看到的那個例子。

先看下它打印出來的字節碼:
---------------------------------
main of "test.lua" (25 bytes at 001720D8)
     0 PUSHFUNCTION 00172168 ; "test.lua":1
     5 STOREGLOBAL 13 ; add
     8 PUSHGLOBAL 7 ; print
    11 PUSHGLOBAL 13 ; add
    14 PUSHBYTE 3
    16 PUSHBYTE 4
    18 CALLFUNC 2 1
    21 CALLFUNC 1 0
    24 RETCODE0
function "test.lua":1 (9 bytes at 00172168); used at main+1
     0 ADJUST 2
     2 PUSHLOCAL0 0 ;
     3 PUSHLOCAL1 1 ;
     4 ADDOP
     5 RETCODE 2
     7 RETCODE 2
---------------------------------

這個主要是爲了對比觀察下面的 dump 的 luac.out,看下 dump 出來的 luac.out 的二進制文件:
---------------------------------
1B 4C 75 61 23 34 12 46 0A BF 17 46 00 00 19 00 00 00 09 00 74 65 73 74 2E 6C 75 61 00 08 68 21 17 00 22 00 00 14 00 00 14 06 00 04 03 04 04 3F 02 01 3F 01 00 40 56 09 00 06 00 70 72 69 6E 74 00 56 0C 00 04 00 61 64 64 00 46 00 00 09 00 01 00 01 00 29 02 09 0A 30 41 02 41 02
---------------------------------
一點點的分析下上面的每一個字節表明的是什麼:
1B 4C 75 61 23 34 12 46 0A BF 17
這部分是 Header,也就是 DumpHeader 保存下來的內容:
1B : 也就是十進制的 27,就是 ID_CHUNK。(上面的都是十六進制的表示,這裏就不在前面加 0x 了。)
4C 75 61 : 字符串 "Lua",SIGNATURE。
23 : 版本號 0x23,VERSION。
34 12 :數字 0x1234,TEST_WORD。
46 0A BF 17 :浮點值 0.123456789e-23,TEST_FLOAT。
----------------
46 00 00 19 00 00 00 09 00 74 65 73 74 2E 6C 75 61 00 08 68 21 17 00 22 00 00 14 00 00 14 06 00 04 03 04 04 3F 02 01 3F 01 00 40 56 09 00 06 00 70 72 69 6E 74 00 56 0C 00 04 00 61 64 64 00
這個是主函數 dump 出來的內容。下面分別說下其中每一個字節的意思:
46 : 字符 'F',函數標籤 ID_FUN。

00 00 19 00:函數的 size,DumpSize 時用了兩個雙字節,這裏是後面的高字節表明實際的低字節,因此大小爲 0x19,恰好是 25,也就是上面的 print 打印出來的字節碼中的 main of "test.lua" (25 bytes at 001720D8) 這裏的 25。

00 00 :主函數的 lineDefined,主函數的話這個值必定是 0 。

09 00 74 65 73 74 2E 6C 75 61 00 : 前面的兩字節 00 09 是後面的字符串長度 9 。後面的字符串 "test.lua",後面的 00 是字符串的結尾 0 。這是 dump 字符串的格式,先 dump 字符串的長度,再 dump 字符串。

08 68 21 17 00 22 00 00 14 00 00 14 06 00 04 03 04 04 3F 02 01 3F 01 00 40 :
這段是字節碼,長度爲 25 字節,也就是函數的 size 。對比上面打印出來的字節碼
==========
08 68 21 17 00 : PUSHFUNCTION 00172168 ; "test.lua":1,08 就是 PUSHFUNCTION。PUSHFUNCTION 在指令 OpCode 裏的枚舉值就是它。後面的就是主函數的內存地址。

22 00 00 : STOREGLOBAL 13 ; add;十六進制的 22 就是 34,也就是 STOREGLOBAL。後面的 13 哪去了?這就是前面咱們所說的 ThreadCode 的功勞了。這裏被清 0 了。

14 00 00 :PUSHGLOBAL 7 ; print;十六進制的 14 就是 20, 也就是 PUSHGLOBAL,後面的 7 被清 0 。注意,這裏的 print 的字節碼偏移量爲 9 ,也就是下面的 dump 字符串裏的 9 的含意。

14 06 00 :PUSHGLOBAL 13 ; add;這個地方爲啥沒有被清 0 ?06 是什麼?這也要歸功於 ThreadCode  。看看這裏也是操做 add 這個符號,也就是和前面的 STOREGLOBAL 13 一樣的符號,也就是上一個 13 在字節碼裏的偏移就是 6 。這裏的 6 就是這麼來的,ThreadCode  就是幹這個事兒的,因此它才叫 ThreadCode (串邊代碼)。注意,這裏的 0006 在字節碼裏的偏移量爲 12,也就是下面的 dump 字符串裏的 000C 的含意。

04 03 :PUSHBYTE 3
04 04 :PUSHBYTE 4
3F 02 01 :CALLFUNC 2 1
3F 01 00 :CALLFUNC 1 0
40 :RETCODE0
==========

56 09 00 06 00 70 72 69 6E 74 00 56 0C 00 04 00 61 64 64 00 :
這部分就是 DumpStrings,下面也分別看一下:
56 09 00 06 00 70 72 69 6E 74 00 :這個就是 dump print 符號的。56 就是字符 'V',0009 就是它出如今字節碼裏的位置,0006 是後面的字符串的長度,後面的就是字符串 "print" 再加上結尾的 0 。
56 0C 00 04 00 61 64 64 00 : 這個就是 dump add 符號的。56 就是字符 'V',000C 就是它最後一次出如今字節碼裏的位置,0004 是後面的字符串的長度,後面的就是字符串 "add" 再加上結尾的 0 。
主函數分析完了。
----------------
再看下 add 函數。
46 00 00 09 00 01 00 01 00 29 02 09 0A 30 41 02 41 02:
46 : 字符 'F',函數標籤 ID_FUN。

00 00 09 00:函數的 size,DumpSize 時用了兩個雙字節,這裏是後面的高字節表明實際的低字節,因此大小爲 0x09,也就是上面的 print 打印出來的字節碼中的 function "test.lua":1 (9 bytes at 00172168); used at main+1,裏的 9 的。

01 00 : 非主函數,dump 它的定義行號 lineDefined,這裏它爲 1 。
01 00 : 非主函數,打印它的 marked,此時爲 1 。

29 02 09 0A 30 41 02 41 02:這部分就是 add 函數的字節碼
29 02 : ADJUST 2
09 :PUSHLOCAL0 0 ; 注意,這裏打印出來的 0 是沒有意義的,只是爲了和 PUSHLOCAL 打印結果保持一致。
0A:PUSHLOCAL1 1 ; 同上。
30:ADDOP
41 02 :RETCODE 2
41 02 :RETCODE 2
由於這個函數沒有用到符號和字符串,因此 DumpStrings 這裏沒有輸出。
函數 add 分析完了。

----------------------------------------
編譯器分析部分到此結束。
下面看看解釋器是如何工做的。
code

相關文章
相關標籤/搜索