4 Lua Binary Chunks
Lua可以将函数转储成二进制块,然后写到文件中,读取和运行。二进制块的行为于编译它的源代码的行为要严格的一致。
一个二进制块包含两部分:一个头和一个顶级函数。头部分包含12个元素:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Header block of a Lua 5 binary chunk 默认数值使用32位小尾字节序(little-endian)平台上的IEEE 754双精度浮点数格式显示。头的尺寸是12字节。
4 bytes 头署名: ESC, “Lua” 或 0x1B 4C 7561 • 通过检查这个头署名来识别二进制块
1 byte 版本号, 0x51 (81 十进制数) for Lua 5.1 • 高位十六进制数字是主版本号 • 低位十六进制数字是次版本号 1 byte 格式版本,0=官方版本 1 byte 字节序标志(默认值 1) • 0=大尾字节序(big endian), 1=小尾字节序(little endian)
1 byte Size of int (in bytes) (default 4) 1 byte Size of size_t (in bytes) (default 4) 1 byte Size of Instruction (in bytes) (default 4) 1 byte Size of lua_Number (in bytes) (default 8) 1 byte Integral flag (default 0) • 0=floating-point, 1=integral number type
On an x86 平台上,默认的头部字节串 (十六进制): 1B 4C 7561 51000104 04040800 |
Lua 5.1二进制块头部总是 12 个字节长。因为Lua虚拟机的特性是硬编码的,Lua执行代码时会检查头部的12个字节来决定这个二进制块适合不适合运行。二进制块所有12个字节必须严格的匹配运行平台的头部字节,否则Lua 5.1拒绝读取二进制块。头部不会受到字节序的影响,同样的代码可以被用来读取头部是小尾字节序或大尾字节序的二进制块。lua_Number的数据类型由lua_Number类型大小的字节和整数标志共同决定。
理论上,Lua的二进制块是可移植的,实际上,对于运行代码来说不需要支持这个特性。如果你需要运行程序读取所有的二进制块,有可能您做错了什么事情。可是不知何故你又需要这个特性,你可以尝试使用ChunkSpy的重写选项,它允许你将二进制块从一种形式转换到另一种形式。
总之,大部分时间都不需要仔细的查看头部,因为既然Lua源代码通常是可用的,那么一个程序块就会很容易的被编译成内部的二进制块。
紧跟着头部的就是块的顶级函数:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Function block of a Lua 5 binary chunk 保存所有与函数相关的数据。这里显示的是顶级函数。
String 源文件名 Integer 起始行 Integer 终止行 1 byte upvalues的数量 1 byte parameters的数量 1 byte is_vararg标志 (查看下面更深入的解释) • 1=VARARG_HASARG • 2=VARARG_ISVARARG • 4=VARARG_NEEDSARG 1 byte 最大的堆栈数目 (寄存器使用的数量)
List 指令列表 (code) List 常量列表 List 函数原型列表 List 源代码行 (调试数据可选项) List 局部变量列表 (调试数据可选项) List upvalues列表 (调试数据可选项) |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
二进制块中的函数定义成函数原型。为了实际运行函数,Lua首先创建一个函数的实例(或闭包)。二进制块中的函数是由一些头元素和一串列表组成。调试数据可以去掉。
String 以这种方式定义:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
所有的字符串都被定义成下面这种格式:
Size_t 字符串数据的大小 Bytes 字符串数据,尾部包含一个NUL (ASCII 0)
字符串数据的大小包括了结尾处的一个NUL字符,所以一个空字符串(“”)的大小为1。大小为零意味着字符串数据的字节数为0;就是字符串不存在。这样的字符串常函数的源文件名域。 |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
source name 通常是源代码文件的名字,二进制块就是从这个文件编译过来的。它也是一个字符串。源文件名只有在顶级函数中被指定;在其他函数中它是由一个为值为0的字符串长度组成。
line defined和last line defined是函数在源代码文件中开始和结束的行号。number of upvalues和number of parameters这两个名字很明显,maximum stack size也是一样。可是is_vararg有些复杂。这就是所有的一个字节大小的域。
is_vararg标志由3位组成,通常Lua5.1定义常量LUA_COMPAT_VARARG允许可变参数函数(vararg functions)使用表arg,表arg本身并不占用函数参数的个数。对于使用arg的老式代码,is_vararg是7。如果代码中可变参数函数使用…而不是arg,is_vararg是3(VARARG_NEEDSARG 域是 0)。如果为了编译兼容 5.0.2 ,is_vararg是2。
总而言之,对于可变参数函数VARARG_ISVARARG (2)总是设置的。如果LUA_COMPAT_VARARG被定义了VARARG_HASARG (1)也被设置。如果…在函数中没有使用VARARG_NEEDSARG (4)被设置。一个普通的函数is_vararg标志总是0,同时这主程序块的is_vararg标志总是2。
在头元素的后面是一些保存信息的列表,它们组成了函数体。每一个列表都由计算列表大小的Integer开头,后面跟着一些列表元素。每一个列表都有它自己的元素格式。大小位0的列表根本就没有元素。
在下面的方框中,数据类型放在方括号中,例如[Integer]意思是有若干个元素,它们都是整型。总数由列表大小给出。圆括号中的名字是Lua源代码中给出的名字;它们是数据结构的成员。
第一个列表是指令列表或是函数的实际代码。这是将要被实际运行的指令的列表:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Instruction list 保存将要被实际运行的指令的列表。
Integer 代码的大小 (sizecode) [Instruction] 虚拟机指令 |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
虚拟机指令的格式将在下一章中给出。代码生成器总是会生成一个RETURN指令,所以指令列表的大小应该至少是1。下一个是常量列表:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Constant list 保存着函数中引用的常量的列表 (常量池)
Integer 常量列表的大小 (sizek) [ 1 byte 常量类型 (value in parentheses): • 0=LUA_TNIL, 1=LUA_TBOOLEAN, • 3=LUA_TNUMBER, 4=LUA_TSTRING Const 常量本身: 如果常量类型是0这个域不存在;如果类型是1这个是0或1;如果类型是3这个域是 Number;如果类型是4 这个域是String。 ] |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Number是Lua的数字数据类型,通常是IEEE 754 64-bit双精度浮点数。Integer, Size_t和Number都是字节序敏感的;Lua5.1将不会读取那些字节序与平台字节序不同的块。当然它们的大小格式都在二进制块的头部详细指定了。Number数据类型由它的字节大小和整数标志决定。布尔值被编码成0或者1。
出现在常量列表后面的是函数原型列表:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Function prototype list 保存着在函数中定义的函数原型。
Integer 函数原型大小 (sizep) [Functions] 函数原型数据,或者是函数块 |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
函数原型或者函数块具有与顶层函数或块一样的的格式。但是那些不是顶层函数的函数原型定义了但是不具有源文件名。通过这种方法,函数原型被定义和嵌套在不同的词汇作用域级别。在一个复杂的二进制块中,嵌套的深度有可能达到数层。一个闭包通过在列表中的号码来引用函数。
函数原型列表后面的列表是可选项。它包含调试信息,为了节省空间可以去掉。第一个出现的是源代码行数位置列表:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Source line position list 保存着函数中的每一体指令的相应的源代码行号。这个信息被用在错误处理和调试中。在没有调试信息的二进制文件中,这个列表的大小位0。函数的运行不依赖于这个列表。
Integer 源代码位置列表的大小 (sizelineinfo) [Integer] 相应的指令位置索引;整数值是产生这条指令的源代码的行号。 |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
下面出现的是局部变量列表。每一个局部变量列表有3个域,一个字符串两个整型:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Local list 保存局部变量名字和程序计数范围,在这个范围内局部变量是活跃的。
Integer 局部变量列表大小(sizelocvars) [ String 局部变量名 (varname) Integer 局部变量作用域的开始 (startpc) Integer 局部变量作用域的结束 (endpc) ] |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
最后一个列表是upvalue列表:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
Upvalue list 保存upvalue名字。
Integer upvalue列表的大小 (sizeupvalues) [String] upvalue名字 |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
所有的列表都不会被共享或重用:在同一个函数中,在代码中引用的局部变量,upvalues,常量,函数原型一定是在它们各自的列表中被指定的。此外,局部变量,upvalues,常量,函数原型都使用从0开始的数值作为索引。在反汇编代码中,源代码行号位置列表和指令列表使用从1开始的索引。注意后者只是一个惯例,索引对于虚拟机本身来说没有一点影响,因为所有与跳转有关的指令都使用有符号的偏移。但是对于调试信息来说,局部变量的作用域使用绝对的程序计数位置来编码,并且这个位置是基于从1开始的索引。这也与luac的输出列表一致。
How does it all fit in?(这句不知道怎么翻译) 使用ChunkSpy你可以很轻松的生成一个详细的二进制块的反汇编代码。键入如下的一小块代码并且将文件命名为sample.lua:
local a = 8
function b(c) d = a + c end
接下来,从命令行运行ChunkSpy并生成列表文件:
$ lua ChunkSpy.lua --source simple.lua > simple.lst
下面是对生成的列表文件(simple.lst)的描述,分成了许多片段:
Pos Hex Data Description or Code
---------------------------------------------------------------------
0000 ** source chunk: simple.lua
** global header start **
0000 1B 4C 7561 header signature: "/27Lua"
0004 51 version (major:minor hex digits)
0005 00 format (0=official)
0006 01 endianness (1=little endian)
0007 04 size of int (bytes)
0008 04 size of size_t (bytes)
0009 04 size of Instruction (bytes)
000A 08 size of number (bytes)
000B 00 integral (1=integral)
* number type: double
* x86 standard (32-bit, little endian,doubles)
** global header end **
这是一个二进制块的头的例子。ChunkSpy将这个头命名为全局头以区分函数头。对于指定某一平台的二进制块,很容易一口气匹配整个头,而不是测试每一个域。就像前面描述的,头的大小是12个字节,需要与平台严格的匹配,否则Lua5.1将不会读取二进制块。
跟在全局头后面的是顶级函数的函数头:
000C ** function [0] definition (level 1)
** start of function **
000C 0B000000 string size (11)
0010 73696D 706C 652E 6C + "simple.l"
0018 756100 "ua/0"
source name: simple.lua
001B 00000000 line defined (0)
001F 00000000 last line defined (0)
0023 00 nups (0)
0024 00 numparams (0)
0025 02 is_vararg (2)
0026 02 maxstacksize (2)
函数头的大小总是可变的,取决于source name字符串。源文件名仅仅在顶级函数中出现。顶级函数没有定义的行号,所以两个行号定义域都是0。没有upvalues和参数。顶级块总是可以使用可变数目的参数;顶级块的is_vararg总是2。对于这个十分简单的块,堆栈的大小被设置成最小是2。
接下来出现的是各种列表,以主块的代码列表开始:
* code:
0027 05000000 sizecode (5)
002B 01000000 [1] loadk 0 0 ; 8
002F 64000000 [2] closure 1 0 ; 1 upvalues
0033 00000000 [3] move 0 0
0037 47400000 [4] setglobal 1 1 ; b
003B 1E008000 [5] return 0 1
源代码的第一行被编译成一条指令,行[1]。局部变量a是寄存器0,数字8是常量0。在行[2]中,一条函数原型0的指令被创建,闭包被临时的放在寄存器1中。实际上CLOSURE指令使用第三行的MOVE指令来管理upvalue a;MOVE没有真是的运行。将在第14章详细的介绍。然后在第[4]行,闭包将被放置在全局变量b;“b”是常量1同时闭包在寄存器1中。行[5]返回控制权到调用函数。在这种情况下,退出块。
常量列表跟随在指令后面:
* constants:
003F 02000000 sizek (2)
0043 03 const type 3
0044 0000000000002040 const [0]: (8)
004C 04 const type 4
004D 02000000 string size (2)
0051 6200 "b/0"
const [1]: "b"
顶级函数需要两个常量,数字8(在第一行的赋值语句中使用)和字符串“b”(第二行被用来引用全局变量b)。
紧跟在后面的是主块的函数原型列表。在源代码的第2行,一个函数原型被声明在主块内。函数被编译成指令函数闭包被赋值给了全局变量b.
函数原型列表保存着所有的相关信息,一个函数块套着一个函数块。ChunkSpy报告这个函数原型为函数原型计数0,级别2。级别1是顶级函数;只有一个级别为1的函数,但是可能有一个或多个其他级别的函数。
* functions:
0053 01000000 sizep (1)
0057 ** function [0] definition (level 2)
** start of function **
0057 00000000 string size (0)
source name: (none)
005B 02000000 line defined (2)
005F 02000000 last line defined (2)
0063 01 nups (1)
0064 01 numparams (1)
0065 00 is_vararg (0)
0066 02 maxstacksize (2)
* code:
0067 04000000 sizecode (4)
006B 44000000 [1] getupval 1 0 ; a
006F 4C 008000 [2] add 1 1 0
0073 47000000 [3] setglobal 1 0 ; d
0077 1E008000 [4] return 0 1
以上是函数b的原型的第一部分。没有源代码名字字符串;定义在第2行(两个值都指向第二行);一个upvalue;有一个参数,c;不是可变参数函数;最大堆栈大小是2。参赛位于从栈底开始的位置,所以函数的唯一一个参数c在寄存器0中。
这个原型有4条指令。大部分的Lua虚拟机指令很容易解释,但是很多具有不太明显的细节。但是这个例子应该十分容易理解。行[1]0是upvalue a,1是目标寄存器,这是一个临时寄存器。行[2]是加法操作,寄存器1保存着临时结果同时寄存器0是函数参数c。行[3]全局变量d (有常量0命名)被设置,并且在下一行,函数返回。
* constants:
007B 01000000 sizek (1)
007F 04 const type 4
0080 02000000 string size (2)
0084 6400 "d/0"
const [0]: "d"
* functions:
0086 00000000 sizep (0)
函数的常量列表只有一个条目,字符串“d”被当作名字用来查询全局变量。下面是源代码行数位置列表:
* lines:
008A 04000000 sizelineinfo (4)
[pc] (line)
008E 02000000 [1] (2)
0092 02000000 [2] (2)
0096 02000000 [3] (2)
009A 02000000 [4] (2)
所有4调指令都是由源代码第2行生成的。
函数原型的最后两个列表是局部变量列表和upvalue列表:
* locals:
009E 01000000 sizelocvars (1)
00A 2 02000000 string size (2)
00A 6 6300 "c/0"
local [0]: c
00A 8 00000000 startpc (0)
00AC 03000000 endpc (3)
* upvalues:
00B0 01000000 sizeupvalues (1)
00B4 02000000 string size (2)
00B8 6100 "a/0"
upvalue [0]: a
** end of function **
有一个局部变量,参数c。对于参数,startpc值是0。定义在函数中的普通的参数startpc值是1。也有一个upvalue,a,用来引用父(上层)函数中的局部变量a。
在函数b的函数原型数据结束后面,带有顶层函数的调试信息的块重新开始了:
* lines:
00BA 05000000 sizelineinfo (5)
[pc] (line)
00BE 01000000 [1] (1)
00C 2 02000000 [2] (2)
00C 6 02000000 [3] (2)
00CA 02000000 [4] (2)
00CE 02000000 [5] (2)
* locals:
00D2 01000000 sizelocvars (1)
00D6 02000000 string size (2)
00DA 6100 "a/0"
local [0]: a
00DC 01000000 startpc (1)
00E0 04000000 endpc (4)
* upvalues:
00E4 00000000 sizeupvalues (0)
** end of function **
00E8 ** end of chunk **
从源代码行数列表我们可以看出顶级函数有5条指令。第一条指令来自于源代码第一行,其他四条指令来自于源代码第二行。
顶级函数有一个局部变量,名字为“a”,从程序计数位置1到位置4活跃,并且引用寄存器0。没有upvalues,所以这个表的大小是0。主块的调试信息列出后,整个二进制块结束了。
现在我们已经查看了二进制块的细节,接下来我们将查看每一条Lua 5.1虚拟机指令。
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
5 Instruction Notation
在查看Lua虚拟机之前,还有一些关于描述指令所使用的符号的事情。在Lua源代码文件lopcodes.h中,指令描述是以一种注释的方式给出的。指令描述重新出现在下面的章节中,添加了解释说明。下面是一些基本的符号:
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
R(A) 寄存器 A (特指指令中域A) R(B) 寄存器 B (特指指令中域B) R(C) 寄存器 C (特指指令中域C) PC 程序计数器 Kst(n) 常量列表中的元素n Upvalue[n] 具有索引n的upvalue名字 Gbl[sym] 由符号sym索引的全局变量 RK(B) 寄存器B或者是常量索引 RK(C) 寄存器C或者是常量索引 sBx 用于所有跳转的有符号偏移(域sBx) |
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CWangBo%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
用于描述指令的符号有一点像伪C。使用在符号中的操作符主要是C操作符,同时条件语句使用C形式的求值方式。所以这种符号是实行指令的实际C代码的一种不精确的翻译。
有些指令的操作不能由一两行符号清晰的描述。因此这本指南将补充符号,这些符号能够详细的描述每条指令的操作。为了描述指令,给出一小片Lua源代码来显示指令如何工作。使用ChunkSpy的交互模式,你可以亲自试验例子并且马上获得反汇编代码形式的输出。如果你想让反汇编列出数据的字节值和指令,你可以使用ChunkSpy产生一个普通的,详细的,反汇编列表。
虚拟机的程序计数器(PC)总是指向小一条指令。这个行为对于大多数微处理器都是标准的。规则是这样的,一旦一条指令被读取并执行,程序计数器马上更新。所以为了跳过当前指令的下一条指令,PC加1(偏移)。一个-1偏移量,理论上导致了一个跳会到自身的JMP指令,这样会导致无限循环。幸运的是,代码生成器不支持会产生这种指令的语句。
像前面说明的一样,寄存器和局部变量近似的相等。临时结果经常保存在寄存器中。对于一些指令,指令的域B和C可以执行常量而不是寄存器,只有当这个域有更多的 MSB (最多的有意义的位)集合。例如,域B的值是256,这将指向索引为0的常量,前提是这个域是9位宽(*8为可以表示0~255,因为寄存器最多为256个所以小于256的数表示寄存器,大于等于256的数使用它与256的差表示常量的索引。*)。对于大多数指令,域A是目标寄存器。反汇编列表保证,A,B,C操作数域的顺序的连贯性。