Lua二进制chunk

Lua是 一门以高效著称的脚本语言,为了达到较高的执行效率,Lua从1.0(1993年发布)开始就内置了虚拟机lvm。也就是说,Lua脚本并不是直接被Lua解释器解释执行的,而是类似于Java那样,先由Lua编译器编译为字节码ByteCode,然后交由Lua虚拟机去执行。Lua字节码ByteCode需要一个载体,这个载体就是二进制chunk,可以将Lua的二进制chunk看做Java的class文件。

chunk

在Lua中一段可以被Lua解释器解释执行的代码叫做chunkchunk可以很小,小到一两条语句。也可以很大,大到包含成千上万语句和复杂的函数定义。为了获得较高的执行效率,Lua并不直接解释执行chunk,而是先由编译器编译成内部结构,其中包含字节码等信息,然后再由虚拟机执行字节码。这种内部结构在Lua里叫做预编译(Precompiled)chunk,由于采用了二进制格式,所以也叫做二进制(Binary)chunk

4933701-ed89db4a7609f331.png
隐式调用Lua编译器

Lua程序员一般无需关心二进制chunk,因为Lua解释器会在内部进行编译。Lua提供了命令行工具luac,可以把Lua源代码编译成二进制chunk,并保存成文件,默认文件名为luac.out。Lua解释器可直接加载并执行二进制chunk文件。

4933701-67ba6e65316d533d.png
显式调用Lua解释器

Lua解释器会在内部编译Lua脚本,所以预编译并不会加快脚本执行的速度,但是预编译可以加快脚本加载的速度,并可以在一定程序上保护源代码。另外luac还提供了反编译功能,方便查看二进制chunk内容和Lua虚拟机指令。

luac

luac命令主要由2个用途:

  1. 作为编译器,把Lua文件编译成二进制chunk文件
  2. 作为反编译器,分析二进制chunk,将信息输出到控制台。

Lua将编译命令和反编译命令整合在一起,在命令行直接执行luac命令可查看其完整用法。

λ luac
luac: no input files given
usage: luac [options] [filenames].
Available options are:
  -        process stdin
  -l       list 查看二进制chunk
  -o name  output to file 'name' (default is "luac.out") 对输出文件进行明确指定
  -p       parse only 仅执行解释,即只是检查语法是否正确,不产生输出文件。
  -s       strip debug information 去掉编译生成的二进制chunk默认包含的调试信息(行号、变量名等)
  -v       show version information 显示版本信息
  --       stop handling options

$ luac test.lua # 生成luac.out
$ luac test.lua -o test.out # 生成test.out
$ luac test.lua -s # 不包含调试信息
$ luac test.lua -p # 只进行语法检查

编译Lua源代码

将一个或多个文件名作为参数调用luac命令就可以编译指定的Lua源文件,若编译成功则在当前目录下生成luac.out文件,其中的内容就是对应的二进制chunk

Lua编译器工作原理

Lua编译器以函数为单位进行编译,每个函数都会被Lua编译器编译称为一个内部结构,这个结构叫做“原型”(prototype),原型主要包含6部分内容,分别是:

  1. 函数基本信息:包含参数数量、局部变量数量等
  2. 字节码
  3. 常量表
  4. Upvalue表
  5. 调试信息
  6. 子函数原型列表

由此可知,函数原型是一种递归结构,Lua源码中函数的嵌套关系会直接反映在编译后的原型中。

print("hello, world!")

上面仅有一条打印语句,并没有定义函数,那么Lua编译器是怎么编译这个文件的呢?由于Lua是脚本语言,如果每执行一段脚本都必须要定义一个函数,不是很麻烦吗?所以这样吃力不讨好的工作就由Lua编译器代劳了。

Lua编译器会自动为脚本添加一个main主函数,并将整个程序都放在这个函数里,然后再以它为起点进行编译,那么自然就把整个程序都编译出来了。主函数不仅仅是编译的起点,也是为了Lua虚拟机解释执行程序时的入口。

程序被Lua编译器加工后,会变成如下:

function main(...)
  print("hello world!")  
  return
end

将主函数编译成函数原型后,Lua编译器会给它再添加一个头部Header,然后一起dump成为luac.out文件,这样二进制chunk文件就产生了。

4933701-923d9abdb616df13.png
二进制chunk内部结构

查看二进制chunk

二进制chunk之所以使用二进制格式,是为了方便虚拟机加载,然后对人类却不够友好,因为其很难直接阅读。luac命令兼具编译和反编译功能,使用luac -l选项可查看二进制chunk,即luac反编译器精简模式的输出类型。

$ vim test.lua
-- test.lua
print("hello world")
$ luac test.lua
$ luac -l luac.out
main <test.lua:0,0> (4 instructions, 16 bytes at 006BECA0)
0+ params, 2 slots, 0 upvalues, 0 locals, 2 constants, 0 functions
        1       [2]     GETGLOBAL       0 -1    ; print
        2       [2]     LOADK           1 -2    ; "hello world"
        3       [2]     CALL            0 2 1
        4       [2]     RETURN          0 1
$ vim test.lua
function foo()
  function bar()
  end
end
$ luac test.lua
$ luac -l test.out
main <test.lua:0,0> (3 instructions, 12 bytes at 0206ECA0)
0+ params, 2 slots, 0 upvalues, 0 locals, 1 constant, 1 function
        1       [4]     CLOSURE         0 0     ; 0206EE10
        2       [1]     SETGLOBAL       0 -1    ; foo
        3       [4]     RETURN          0 1

function <test.lua:1,4> (3 instructions, 12 bytes at 0206EE10)
0 params, 2 slots, 0 upvalues, 0 locals, 1 constant, 1 function
        1       [3]     CLOSURE         0 0     ; 0206EE78
        2       [2]     SETGLOBAL       0 -1    ; bar
        3       [4]     RETURN          0 1

function <test.lua:2,3> (1 instruction, 4 bytes at 0206EE78)
0 params, 2 slots, 0 upvalues, 0 locals, 0 constants, 0 functions
        1       [3]     RETURN          0 1

使用luac -l反编译打印的函数信息包含两个部分:前两行是函数基本信息,后面是指令列表

main <test.lua:0,0> (4 instructions, 16 bytes at 006BECA0)

function <test.lua:1,4> (3 instructions, 12 bytes at 0206EE10)

函数 <源文件名:起始行号,终止行号>(指令数量, 函数地址)

第1行:若以main开头说明编译器自动生成主函数,若以function开头则说明是一个普通函数。接着是定义函数的源文件名和函数在文件里的起止行号(对于主函数,起止行号都是0),然后是指令数量和函数地址。

第2行:依次给出函数的固定参数数量(若有+号表示是一个vararg函数)、运行函数所必要的寄存器数量、upvalue数量、局部变量数量、常量数量、子函数数量。

0+ params, 2 slots, 0 upvalues, 0 locals, 1 constant, 1 function
参数数量, 寄存器数量, upvalue数量, 局部变量数量, 常量数量, 子函数数量

指令列表中每条指令都包含指令序号、对应行号、操作码、操作数。分号后面是luac根据指令操作数生成的注释,以便于理解指令。

指令序号, 对应行号, 操作码, 操作数, 注释
1       [4]     CLOSURE         0 0     ; 0206EE10
2       [1]     SETGLOBAL       0 -1    ; foo
3       [4]     RETURN          0 1

luac反编译器详细模式输出内容,luac会将常量表、局部变量表、upvalue表信息也打印出来。

$ luac -l -l luac.out

main <test.lua:0,0> (3 instructions, 12 bytes at 01FBECA0)
0+ params, 2 slots, 0 upvalues, 0 locals, 1 constant, 1 function
        1       [4]     CLOSURE         0 0     ; 01FBEE28
        2       [1]     SETGLOBAL       0 -1    ; foo
        3       [4]     RETURN          0 1
constants (1) for 01FBECA0:
        1       "foo"
locals (0) for 01FBECA0:
upvalues (0) for 01FBECA0:

function <test.lua:1,4> (3 instructions, 12 bytes at 01FBEE28)
0 params, 2 slots, 0 upvalues, 0 locals, 1 constant, 1 function
        1       [3]     CLOSURE         0 0     ; 01FBEE90
        2       [2]     SETGLOBAL       0 -1    ; bar
        3       [4]     RETURN          0 1
constants (1) for 01FBEE28:
        1       "bar"
locals (0) for 01FBEE28:
upvalues (0) for 01FBEE28:

function <test.lua:2,3> (1 instruction, 4 bytes at 01FBEE90)
0 params, 2 slots, 0 upvalues, 0 locals, 0 constants, 0 functions
        1       [3]     RETURN          0 1
constants (0) for 01FBEE90:
locals (0) for 01FBEE90:
upvalues (0) for 01FBEE90:

chunk格式

Lua的二进制chunk本质上也是一个字节流

  1. 二进制chunk格式属于Lua虚拟机内部实现细节,并未标准化,也没有官方文档说明,一切以Lua官方实现的源码为准。
  2. 二进制chunk格式的设计没有考虑跨平台的需求,对于需要使用一个字节表示的数据,必须要考虑大小端(Endianness)问题。Lua官方实现的做法是编译Lua脚本时,直接按照本机的大小端方式生成二进制chunk文件,当加载二进制chunk文件时,会探测被加载文件的大小端方式,如果和本机不匹配就拒绝加载。
  3. 二进制chunk格式的设计没有考虑Lua版本兼容性,Lua官方做法是编译Lua脚本时,直接按照当时的Lua版本生成chunk文件,当加载二进制chunk文件时,会检测被加载文件的版本号,如果和当前Lua版本不一致则拒绝加载。
  4. 二进制chunk格式的设计没有刻意设计的很紧凑,在某些情况下一段Lua脚本被编译成二进制chunk后甚至会比文本形式的源文件还要大。由于把Lua脚本编译成二进制chunk的主要目的是为了获得更快的加载速度,所以这也不是什么大问题。

数据类型

二进制chunk本质上来说是一个字节流,一个字节能够表示的信息是非常有限的,如一个ASCII码或一个很小的整数可以放进一个字节内,但是更复杂的信息就必须通过某种编码方式编码成多个字节。在讨论二进制chunk格式时,称这种被编码为一个或多个字节的信息单位为数据类型。

由于Lua官方实现是使用C语言编写的,所以C语言的一些数据类型会直接反映在二进制chunk的格式里。二进制chunk内部使用的数据类型大致分为数字、字符串、列表三种。

  1. 数字

数字类型主要包括5种:

  • 字节:用来存放一些比较小的整数值,比如Lua版本号、函数的参数个数等。
  • C语言cint整型:主要用来表示列表长度
  • C语言size_t类型:主要用来表示长字符串长度
  • Lua整数:Lua整数和Lua浮点数则主要在常量表里出现,记录Lua脚本中出现的整数和浮点数字面量。
  • Lua浮点数

数字类型在二进制chunk里都按照固定长度存储,除字节类型外,其余4种数字类型都会占用多个字节,具体占用字节数则会及记录在头部中。

4933701-b5ebf628d396d6c2.png
二进制chunk整数类型
  1. 字符串

字符串在二进制chunk中其实是一个字节数组,因为字符串长度不固定,所以需要将字节数组的长度也记录到二进制chunk中。作为优化,字符串类型又可以进一步分为短字符串和长字符串,具体有3种情况:

  • 对于NULL字符串只用0x00表示即可
  • 对于长度小于等于2530xFD的字符串,先使用一个字节记录长度+1,然后是字节数组。
  • 对于长度大于等于254oxFE的字符串,第一个字节是oxFF,其后是size_t记录长度+1,最后是字节数组。
4933701-7c40c34d5c0ae7eb.png
字符串存储格式
  1. 列表

在二进制chunk内部,指令表、常量表、子函数原型表等信息都按照列表的方式存储。即先用一个cint类型记录列表长度,然后紧接着存储n个列表元素,至于列表元素如何存储需具体情况具体分析。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值