Lua字节码分析

一、lua语句的执行过程

lua代码首先会被lua编译器(通常所指的luac)编译为二进制文件,编译是以一个lua文件为单位,比如一个lua文件内有语法错误,即使你不执行相关代码,这个文件也是编译不过去的;编译完成后,然后二进制文件被lua虚拟机加载执行。

但实际上我们并不能看到编译器的这个过程,这是因为lua解释器是隐式调用编译器和lvm的,也就是lua的编译和执行是被封装起来的,对外部是完全透明的;具体过程如下图所示:

预编译

提前将lua文件编译为二进制文件,也就是二进制chunk。

预编译的主要优点:更快的加载速度,保护源代码;

编译成二进制chunk只能提高lua虚拟机的加载速度,执行速度还是不变的。

二、luac相关

1.luac基础命令

使用luac可以编译生成一个二进制文件,如luac main.lua,这里没有指定输出文件,它就会默认输出文件名luac.out;

luac主要有两个用途:

  • 作为编译器,将Lua源文件编译成二进制chunk文件;
  • 作为反编译器,分析二进制chunk,将分析结果输出到控制台;

luac相关命令

luac -l [filename],生成已编译字节码的列表,如果没有给出文件名,则默认加载luac.out并列出字节码;使用luac -l -l [filename]可以得到更详细的信息

luac -o filename,指定输出文件名,如果只是luac,那么会默认生成luac.out;

luac -p,加载文件但不生成任何输出文件。主要用于语法检查和测试预编译块,只起到一个编译检查的作用。

luac -s,在写入文件之前剥离调试信息;

luac -v,显示版本信息;

2.简单示例

如下所示,新建一个lua文件,输入一条语句print('hello world'),然后进行编译和反编译,反编译结果如下:

 编译器在对一个lua源文件进行编译时,会自动为脚本添加一个main函数,并且把整个程序都放进这个函数里,然后再以它为起点进行编译。这个函数不仅是编译的起点,也是未来Lua虚拟机解释执行程序时的入口,我们写的上述print语句就会变成下面的样子:

function main(...)print('hello world')return
end

反编译打印出的函数信息分为两个部分:前面两行是函数基础信息,后面是指令列表;

函数基础信息

如下是一个测试源代码和反编译结果,用于解读函数基础信息;

print('hello world')
name = 'xxx'function test(arg1, ...)local a = arg1print(name)local tips = 'I am test'print(tips)
end

第一行如果是main,说明是编译器为我们添加的主函数,如果是function,则说明是一个普通函数;跟在后面的是源文件名(文件路径)和函数在文件里的起止行号,对于主函数,起止都是0,对于自定义函数,就是文件内的行号(不过由于我的源文件里前面有几行注释,所以这里的起止和上面的源码没有对应上,但和我本地的行号是对应的);最后就是指令数量和函数的地址;

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

可以使用luac -l -l打印详细信息:

 三、二进制chunk格式

Lua中的二进制chunk格式也只是一个字节流,该格式属于Lua虚拟机内部实现细节;

chunk格式没有考虑跨平台需求,编译脚本时,直接按照本机的大小端方式生成二进制文件,当加载二进制chunk文件时,会探测被加载文件的大小端方式,如果和本机不匹配,就拒绝加载。

二进制chunk格式的设计也没有考虑不同Lua版本之间的兼容问题,编译时直接按照当时的Lua版本生成二进制chunk文件,当加载二进制chunk文件时,会检测被加载文件的版本号,如果和当前Lua版本不匹配,则拒绝加载。

二进制chunk格式没有被刻意得设计很紧凑,编译成二进制chunk之后,甚至会比文本形式的源文件还要大,但把Lua脚本预编译成二进制chunk的主要目的是为了获得更快的加载速度,所以这并没有什么影响。

本文链接:https://my.lmcjl.com/post/1817.html

展开阅读全文

4 评论

留下您的评论.