码迷,mamicode.com
首页 > 其他好文 > 详细

Lua2.4 保存字节码 dump.c

时间:2014-10-13 01:03:39      阅读:622      评论:0      收藏:0      [点我收藏+]

标签:io   使用   ar   for   文件   数据   sp   on   cti   

严格意义上说,把 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。

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 分析完了。

----------------------------------------
编译器分析部分到此结束。
下面看看解释器是如何工作的。

Lua2.4 保存字节码 dump.c

标签:io   使用   ar   for   文件   数据   sp   on   cti   

原文地址:http://my.oschina.net/xhan/blog/327336

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!