2.2 luac命令介绍
luac命令主要有两个用途:第一,作为编译器,把Lua源文件编译成二进制chunk文件:第二,作为反编译器,分析二进制chunk,将信息输出到控制台。这里仍然以Java为对照,JDK提供了单独的命令行工具javap,用来反编译class文件,而Lua则是将编译命令和反编译命令整合在了一起。在命令行里直接执行luac命令(不带任何参数)可以看到luac命令的完整用法。
$ luac luac: no input files given usage: luac [options] [filenames] Available options are: -l list (use -l -l for full listing) -o name output to file 'name' (default is "luac.out") -p parse only -s strip debug information -v show version information -- stop handling options - stop handling options and process stdin
本节主要以“Hello, World! ”程序为例讨论luac命令的两种用法。请读者在$LUAGO/lua/ch02/目录下创建hello_world.lua文件,并且在里面输入如下代码。
print("Hello, World! ")
为了便于讨论,我们暂时将当前路径切换到$LUAGO/lua/ch02/目录。
$ cd $LUAGO/lua/ch02
2.2.1 编译Lua源文件
将一个或者多个文件名作为参数调用luac命令就可以编译指定的Lua源文件,如果编译成功,在当前目录下会出现luac.out文件,里面的内容就是对应的二进制chunk。如果不想使用默认的输出文件,可以使用“-o”选项对输出文件进行明确指定。编译生成的二进制chunk默认包含调试信息(行号、变量名等),可以使用“-s”选项告诉luac去掉调试信息。另外,如果仅仅想检查语法是否正确,不想产生输出文件,可以使用“-p”选项进行编译。下面是luac的一些用法示例。
$ luac hello_world.lua # 生成luac.out $ luac -o hw.luac hello_world.lua # 生成hw.luac $ luac -s hello_world.lua # 不包含调试信息 $ luac -p hello_world.lua # 只进行语法检查
为了方便后面的讨论,本节还会简单介绍一下Lua编译器的内部工作原理,本书第二部分(第14~17章)会详细介绍Lua编译器的实现细节。
Lua编译器以函数为单位进行编译,每一个函数都会被Lua编译器编译为一个内部结构,这个结构叫作“原型”(Prototype)。原型主要包含6部分内容,分别是:函数基本信息(包括参数数量、局部变量数量等)、字节码、常量表、Upvalue表、调式信息、子函数原型列表。由此可知,函数原型是一种递归结构,并且Lua源码中函数的嵌套关系会直接反映在编译后的原型里。
细心的读者一定会想到这样一个问题:前面我们写的“Hello, World! ”程序里面只有一条打印语句,并没有定义函数,那么Lua编译器是怎么编译这个文件的呢?由于Lua是脚本语言,如果我们每执行一段脚本都必须要定义一个函数(就像Java那样),岂不是很麻烦?所以这个吃力不讨好的工作就由Lua编译器代劳了。
Lua编译器会自动为我们的脚本添加一个main函数(后文称其为主函数),并且把整个程序都放进这个函数里,然后再以它为起点进行编译,那么自然就把整个程序都编译出来了。这个主函数不仅是编译的起点,也是未来Lua虚拟机解释执行程序时的入口。我们写的“Hello, World! ”程序被Lua编译器加工之后,就变成了下面这个样子。
function main(...) print("Hello, World! ") return end
把主函数编译成函数原型后,Lua编译器会给它再添加一个头部(Header,详见2.3.3节),然后一起dump成luac.out文件,这样,一份热乎的二进制chunk文件就新鲜出炉了。综上所述,函数原型和二进制chunk的内部结构如图2-3所示。
图2-3 二进制chunk内部结构
2.2.2 查看二进制chunk
二进制chunk之所以使用二进制格式,是为了方便虚拟机加载,然而对人类却不够友好,因为其很难直接阅读。如前所述,luac命令兼具编译和反编译功能,使用“-l”选项可以将luac切换到反编译模式。正如javap命令是查看class文件的利器,luac命令搭配“-l”选项则是查看二进制chunk的利器。本节的目标是学会阅读luac的反编译输出。在2.3节,我们将深入到二进制chunk的内部来研究其格式。
以前面编译出来的hello_world.luac文件为例,其反编译输出如下。
$ luac -l hello_world.luac main <hello_world.lua:0,0> (4 instructions at 0x7fb4dbc030f0) 0+ params, 2 slots, 1 upvalue, 0 locals, 2 constants, 0 functions 1 [1]GETTABUP 0 0-1 ; _ENV "print" 2 [1]LOADK 1-2 ; "Hello, World! " 3 [1]CALL 0 2 1 4 [1]RETURN 0 1
上面的例子以二进制chunk文件为参数,实际上也可以直接以Lua源文件为参数,luac会先编译源文件,生成二进制chunk文件,然后再进行反编译,产生输出。由于“Hello, World! ”程序只有一条打印语句,所以编译出来的二进制chunk里也只有一个主函数原型(没有子函数),因此反编译输出里也只有主函数信息。如果我们的Lua程序里有函数定义,那么luac反编译器会按顺序依次输出这些函数原型的信息,例如如下的Lua程序(请读者将其保存在$LUAGO/lua/ch02/foo_bar.lua文件中)。
function foo() function bar() end end
反编译输出中会依次包含main、foo和bar函数的信息,如下所示。
$ luac -l foo_bar.lua main <foo_bar.lua:0,0> (3 instructions at 0x7fc43fc02b20) 0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function 1 [4] CLOSURE 0 0 ; 0x7fc43fc02cc0 2 [1] SETTABUP 0-1 0 ; _ENV "foo" 3 [4] RETURN 0 1 function <foo_bar.lua:1,4> (3 instructions at 0x7fc43fc02cc0) 0 params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function 1 [3] CLOSURE 0 0 ; 0x7fc43fc02e40 2 [2] SETTABUP 0-1 0 ; _ENV "bar" 3 [4] RETURN 0 1 function <foo_bar.lua:2,3> (1 instruction at 0x7fc43fc02e40) 0 params, 2 slots, 0 upvalues, 0 locals, 0 constants, 0 functions 1 [3] RETURN 0 1
反编译打印出的函数信息包含两个部分:前面两行是函数基本信息,后面是指令列表。
第一行如果以main开头,说明这是编译器为我们生成的主函数;以function开头,说明这是一个普通函数。接着是定义函数的源文件名和函数在文件里的起止行号(对于主函数,起止行号都是0),然后是指令数量和函数地址。
第二行依次给出函数的固定参数数量(如果有+号,表示这是一个vararg函数)、运行函数所必要的寄存器数量、upvalue数量、局部变量数量、常量数量、子函数数量。如果读者看不懂这些信息也没有关系,我们在后面的章节中会陆续介绍这些信息。
指令列表里的每一条指令都包含指令序号、对应行号、操作码和操作数。分号后面是luac根据指令操作数生成的注释,以便于我们理解指令。第3章会详细介绍Lua虚拟机指令。
以上看到的是luac反编译器精简模式的输出内容,如果使用两个“-l”选项,则可以进入详细模式,这样,luac会把常量表、局部变量表和upvalue表的信息也打印出来。
$ luac -l -l hello_world.lua main <hello_world.lua:0,0> (4 instructions at 0x7fbcb5401c00) 0+ params, 2 slots, 1 upvalue, 0 locals, 2 constants, 0 functions 1 [1] GETTABUP 0 0-1 ; _ENV "print" 2 [1] LOADK 1-2 ; "Hello, World! " 3 [1] CALL 0 2 1 4 [1] RETURN 0 1 constants (2) for 0x7fbcb5401c00: 1 "print" 2 "Hello, World! " locals (0) for 0x7fbcb5401c00: upvalues (1) for 0x7fbcb5401c00: 0 _ENV 1 0
到这里luac命令反编译模式的基本用法和阅读方法就介绍完毕了,如果读者觉得一头雾水也不要担心,暂时只要对二进制chunk有一个粗略的认识就可以了,在2.3节我们会详细地讨论二进制chunk格式。