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

隐式调用Lua编译器

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

显式调用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文件就产生了。

二进制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种数字类型都会占用多个字节,具体占用字节数则会及记录在头部中。

二进制chunk整数类型
  1. 字符串

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容