该系列是笔者在学习张秀宏编写的《自己动手实现Lua:虚拟机、编译器、标准库》过程中的笔记总结
本章需要的目录结构和编译环境:
$ cd $LUAGO/go/
$ cp -r ch01/ ch02
$ mkdir ch02/src/luago/binchunk
$ export GOPATH=$PWD/ch02
$ mkdir $LUAGO/lua/ch02
Lua的二进制文件chunk跟Java的class文件类似,本质上是一个字节流。
- 二进制文件chunk格式属于Lua虚拟机内部的实现细节,并没有标准化,也没有任何官方文档对其进行说明,一切以Lua官方的源代码为准。
- 二进制chunk格式的设计没有考虑跨平台的需求。对于需求超过一个字节表示的数据,必须要考虑大小端(Endianness)问题。Lua官方实现的方法是:在编译Lua脚本的时候,直接按照本地的大小端方式生成二进制chunk文件,当加载二进制chunk文件时,会探测被加载文件的大小端方式,如果和本地不匹配,就拒绝加载。
- 二进制chunk格式的设计也不考虑不同Lua版本之间的兼容问题。Lua官方实现的做法是编译Lua脚本时,直接按当时的Lua版本生成二进制chunk文件,当加载二进制chunk文件时,会检测被加载文件的版本号,如果和当前Lua版本不匹配,则拒绝加载。
- 二进制chunk格式并没有刻意设计得很紧凑。在某些情况下,一段Lua脚本被编译成二进制chunk之后,甚至会比文本性质得源文件还要大。
二进制chunk内部使用的数据类型大致可以分为数字,字符串和列表三种。
数字
数字类型主要包括字节、C语言整型(cint)、C语言size_t类型、Lua整型、Lua浮点数五种。其中,字节类型用来存放一些比较小的整数值,比如Lua版本号、函数的参数个数等;cint类型主要用来表示列表长度;size_t表示长字符串长度;Lua整数和Lua浮点数则主要在常量表里出现,记录Lua脚本中出现的整数和浮点数字面量。
数字类型在二进制chunk里都按照固定长度存储。除字节类型外,其余四种数字类型都占用多个字节,具体占用几个字节则会记录在头部里。
二进制chunk格式
字符串
字符串在二进制chunk里实质是一个字节数组,字符串类型进一步优化可以分为短字符串和长字符串两种,具体有三种情况
- 对于null字符串,只用0x00表示就可以了。
- 对于长度小于等于253(0xFD)的字符串,先使用一个字节记录长度+1,然后是字节数组。
- 对于长度大于等于254(0xFE)的字符串,第一个字节是0xFF,后面跟一个size_t记录长度+1,最后是字节数组。
字符串存储格式
列表
在二进制chunk内部,指令表、常量表、子函数原型表等信息都是按照列表方式存储的。先是用一个cint类型记录列表长度,然后紧接着存储n个列表元素,列表元素如何存储需要具体情况具体分析。
总体结构
二进制chunk分为头部和主函数原型两部分。在$LUAGO/go/ch02/src/luago/binchunk目录下创建binary_chunk.go文件,在里面定义binaryChunk结构体。
package binchunk
type binaryChunk struct {
header // 头部
sizeUpvalues byte // 主函数upvalue数量
mainFunc *Prototype // 主函数原型
}
头部
头部总共占用约30个字节(因平台而异),其中包含签名、版本号、格式号、各种整数类型占用的字节数,以及大小端和浮点数格式识别信息等。在binary_chunk.go文件里定义header结构体,代码如下所示:
type
签名
很多二进制格式都会以固定的魔数(Magic Number)开始,Lua二进制chunk的魔数(又叫做签名)大小是四字节,分别是ESC、L、u、a的ASCII吗。用十六进制表示是0x1B4C7561,写成Go语言字符串字面量是“x1bLua”。
魔数主要起快速识别文件格式的作用。如果Lua虚拟机试图加载一个“号称”二进制chunk的文件,如果发现其并非以0x1B4C7561开头,就会拒绝加载该文件。
$ xxd -u -g 1 hello_world.luac
00000000: 1B 4C 75 61 53 00 19 93 0D 0A 1A 0A04 08 04 08 .LuaS...........
00000010: 08 78 56 00 00 00 00 00 00 00 00 00 00 00 28 77 .xV...........(w
00000020: 40 01 11 40 68 65 6C 6C 6F 5F 77 6F 72 6C 64 2E @..@hello_world.
00000030: 6C 75 61 00 00 00 00 00 00 00 00 00 01 02 04 00 lua.............
00000040: 00 00 06 00 40 00 41 40 00 00 24 40 00 01 26 00 ....@.A@..$@..&.
00000050: 80 00 02 00 00 00 04 06 70 72 69 6E 74 04 0E 48 ........print..H
00000060: 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 01 00 00 00 ello, World! ....
00000070: 01 00 00 00 00 00 04 00 00 00 01 00 00 00 01 00 ................
00000080: 00 00 01 00 00 00 01 00 00 00 00 00 00 00 01 00 ................
00000090: 00 00 05 5F 45 4E 56 ..._ENV
版本号
签名之后的一个字节,记录二进制chunk文件所对应的Lua版本号。Lua语言的版本号由三部分组成:大版本号(Major Version)、小版本号(Minor Version)、发布号(Release Version)。例如lua5.3.4,本别对应大版本号,小版本号和发布号
二进制chunk里存放的版本号是根据Lua大小版本号算出来的,其值等于大版本号乘以16加小版本号。Lua虚拟机在加载二进制chunk文件时,先检查其版本号,如果版本号不匹配,就拒绝加载该文件。
格式号
版本号之后的一个字节记录二进制chunk格式号。Lua虚拟机在加载二进制chunk时,会检查其格式号,如果和虚拟机本身的格式号不匹配就拒绝加载该文件,官方实现使用的格式号是0。
LUAC_DATA
格式号之后的6个字节在Lua官方实现里叫做LUAC_DATA。其中前两个字节是0x1993,这是Lua1.0发布的年份;后四个字节依次是回车符(0xD)、换行符(0x0A)、替换符(0x1A)和另外一个换行符,写成Go语言字面量是"x19x93rnx1an"
其作用是起到进一步校验,如果与预期不一样,则认为文件已经损坏,拒绝加载。
整数和Lua虚拟机指令宽度
接下来的五个字节分别记录cint、size_t、Lua虚拟机指令、Lua整数和Lua浮点数这5种数据类型在二进制chunk里占用的字节数。Lua虚拟机在加载二进制chunk时,会检查上述5种数据类型所占用的字节数,如果和期望值不匹配则拒绝加载。
LUAC_INT
接下来的n个字节存放Lua整数值0x5678。存储这个Lua整数的目的是为了检测二进制chunk的大小端方式。
LUAC_NUM
头部的最后n个字节是lua浮点数370.5,存储这个Lua浮点数的目的是为了检测二进制chunk所使用的浮点数格式。Lua虚拟机在加载二进制chunk时,会利用这个数据检查浮点数格式是否和本机匹配,如果不匹配则拒绝加载。目前主流的平台和语言一般都采用IEEE754浮点数据格式。