Lua53二进制格式分析
Lua二进制块
虽然Lua是脚本语言,但是和Python、Ruby等脚本语言类似,Lua实现也是先把脚本编译成二进制的内部格式,然后再交给Lua虚拟机来执行。通常我们使用lua命令直接执行文本格式的Lua脚本即可,编译过程完全被隐藏了起来。但是Lua发行版也提供了luac命令,可以将脚本编译成二进制格式并保存为磁盘文件。在Lua的术语中,脚本的编译单位叫做块(Chunk)。相应的,编译后的二进制格式叫做二进制块(Binary Chunk)。
基本数据类型
和任何二进制格式(例如Java类文件格式、WebAssembly模块二进制格式等)一样,本质上,Lua二进制块就是一个字节流(或者字节数组)。在这个字节流内部,连续N个字节可以构成更大一些的基本数据类型,例如整数、浮点数等。基本数据类型可以构成更复杂的数据结构,例如字符串、列表等。基本数据类型和数据结构又可以构成更复杂的结构,最终构成整个二进制块。下表列出了Lua 5.3和5.4二进制格式的基本数据类型:
主要说明几点。第一,int、lua_Integer、lua_Number等基本类型属于定长类型,在字节流中由固定数量的连续多个字节构成。这些字节如何排列至关重要,这就是我们所熟知的字节序问题。为了保证平台无关性,Java类文件格式和WebAssembly模块二进制格式都对字节序进行了约定。Java类文件采用大端(Big-endian)字节序,WebAssembly二进制模块则采用小端(Little-endian)字节序。Lua二进制块的设计完全没有考虑跨平台,因此直接使用了机器的字节序。此外,除了byte和Instruction类型,定长数据类型的长度也并非完全固定,而是机器相关或编译时可配置的。
第二,Lua 5.3二进制格式并没有考虑紧凑性,因此所有基本数据类型都是定长类型。Lua 5.4在这方面作出了改进,引入了变长整数类型varint,并且不再直接使用来自C语言的int和size_t类型。这是Lua 5.4二进制格式最核心的变化,所以我们先来看看varint类型的编码格式。
lua字符串转数组变长整数类型
Lua二进制块中存储了很多整数,例如调试用的行号、字符串和各种列表的长度等。这些整数通常都很小,因此不管三七二十一都占用固定(比如8个)字节就有点浪费了。为了让二
进制块更紧凑,Lua 5.4引入了变长整数编码。我们所熟知的Protobuf序列化格式,以及前文提到的WebAssembly模块二进制格式都采用了LEB128来编码变长整数以便节约空间。Lua 5.4采用的也是类似的编码格式,但略有不同,下面附上核心的解码函数的代码:
通过阅读解码函数可知,Lua 5.4二进制块varint编码格式和LEB128相比主要有两点不同。第一,LEB128是小端字节序,Lua则是大端字节序。第二,两种编码格式都是利用字节的MSB(Most Significant Bit)来标识是否有后续字节,不过在LEB128中MSB为1表示有后续字节,Lua则刚好相反。下面通过一个例子来说明LEB128和Lua 5.4二进制块varint编码格式上的差异。
假设有一个整数N,可以用3个字节(24个比特)来表示。那么LEB128编码的第一步是把这24个比特分为四组,每组7个比特。于是3个字节变成了4个字节,剩余的空间补0。由于LEB128是小端在前,所以第二步是把上一步得到的4个字节反转顺序。第三步是设置反转后4个字节的MSB,除最后一个字节外,前面字节的MSB都设置成1。编码完毕
如果要存储连续N个相同类型的数据,通常的做法是先记录数据的数量,然后记录N个数据。这种结构在Java类文件格式中叫做表(Table),在WebAssembly模块二进制格式中叫
做向量(Vector)。Lua源代码中并没有给这种结构正式命名,为了便于描述,本文称之为列表(List)。
我们都知道,Java虚拟机规范对Java类文件格式进行了定义,WebAssembly核心规范对WebAssembly模块二进制格式进行了定义。与这些由标准定义的技术不同,Lua二进制块格式完全属于实现细节,没有相应的规范,也不保证向后兼容性。因此Lua二进制块格式最权威的定义就是Lua官方实现的C语言源代码。为了便于理解,本文将采用与Java虚拟机规范中描述Java类文件格式类似的语法来描述Lua二进制块格式。这种描述和C语言中结构体的定义很像,为了便于对比差异,本文会同时给出Lua 5.3和5.4的格式描述。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论