Lua

反编译Lua二进制文件与Lua字节码分析

Spread the love

最近一两个礼拜都在研究lua语言,希望最终自己能够写出一个类似于市面上uLua与Tolua之类的lua互操作库。

但是要操作lua首先就必须了解lua运行的原理。操作lua的最基本数据结构还是堆栈,其实挺像C#与Java的字节码,只不过C#和Java出产品的时候就已经编译好了字节码,而lua则需要编译之后储存到内存当中再进行运行。

所以我们其实同样可以通过反编译的方式来分析lua在运行时的行为。就像分析C#的IL与Java的bytecode一样。

在反编译lua的过程当中最多用到的工具是ChunkSpy,以前的教程当中都说要运行lua控制台才能执行,现在我去下载的时候发现已经是一个独立的exe了。不过其实都一样。

我们编写了最基本的lua代码进行分析。

其实这个例子也是从《Lua虚拟机指令指南》上面拿下来的,我也是跟着作者的思路一步步走下来的。

我们使用cmd指令:ChunkSpy –source ChunkTest.lua进行反编译代码。

结果如下:

我们可以看到输出的指令分为了三栏:

第一栏为位置,也就是目前处于第几个byte,例如0004就是当前位置是第四个byte。

第二栏为十六进制数据,反正也看不懂,我是不看的,大家如果打开二进制查看器的话看到的也就是这些乱码。

第三栏就是最重要的描述信息了,详细描述了这块数据描述了什么东西,我们接下来也会着重针对这块来进行讲解。

我们也可以通过cmd中的”>”命令将输出写入到其他文件当中后然后慢慢看。

Pos Hex Data Description or Code
————————————————————————
0000 ** source chunk: ChunkTest.lua //以**开头的都是注释,可以认为是没有任何代码效用的

//首先我们可以看到整个代码头,一般代码头不需要过多在意,因为基本上lua版本正确,都是可以正常跑起来的。
** global header start **
0000 1B4C7561 header signature: “\27Lua”//二进制块检查签名,类似于Java的class文件中的CAFEBABE的位置,总共4字节
0004 50 version (major:minor hex digits)//版本号
0005 01 endianness (1=little endian)//字节序,1=小头

//下面的都是各种各样的尺寸,由于我的Lua版本是5.3.4的关系,后面几个参数的尺寸与指南中给出的并不一样,不过不用过多在意
0006 04 size of int (bytes)//int的尺寸
0007 04 size of size_t (bytes)//size_t的尺寸
0008 04 size of Instruction (bytes)//指令的尺寸
0009 06 size of OP (bits)//…
000A 08 size of A (bits)//…
000B 09 size of B (bits)//…
000C 09 size of C (bits)//…
000D 08 size of number (bytes)//…
000E B6099368E7F57D41 sample number (double)
* x86 standard (32-bit, little endian, doubles)
** global header end **

//代码头结束

//接下去我们会看到的是顶部函数块的定义
0016 ** function [0] definition (level 1)//顶部函数深度为1
** start of function **
0016 0E000000 string size (14)//源代码名长度
001A 4368756E6B546573+ “ChunkTes”//源代码名
0022 742E6C756100 “t.lua\0″//源代码名
source name: ChunkTest.lua//可以看到给出的源代码名就是ChunkTest.lua
0028 00000000 line defined (0)//定义函数的位置,因为是顶部函数所以肯定就是0了
002C 00 nups (0)//upvalue的数量为0(upvalue类似于C#中的变量捕捉,将外部的局部变量捕捉到闭包当中)
002D 00 numparams (0)//参数数量
002E 00 is_vararg (0)//是否为可变参数
002F 02 maxstacksize (2)//最大所需的栈深度
* lines:
0030 05000000 sizelineinfo (5)//总共用到的字节码数量

//下面是行数信息,用于调试,可以看到每一个指令所在的行号,我们可以看到第一行只有一个指令,而后面4行则有4个指令。
[pc] (line)
0034 01000000 [1] (1)
0038 02000000 [2] (2)
003C 02000000 [3] (2)
0040 02000000 [4] (2)
0044 02000000 [5] (2)

//局部变量列表
* locals:
0048 01000000 sizelocvars (1)//局部变量个数
004C 02000000 string size (2)//局部变量名 尺寸
0050 6100 “a”//实际上为“a\0”所以长度为2
local [0]: a
0052 01000000 startpc (1)//作用域开始的指令序号
0056 04000000 endpc (4)//作用域结束的指令序号

//upvalue列表
* upvalues:
005A 00000000 sizeupvalues (0)//顶部函数一般是不会有upvalue的,下面的闭包函数中我们会看到会有upvalue

//常量列表
* constants:
005E 02000000 sizek (2)//总共有两个常量
0062 03 const type 3//常量的种类,具体有哪些类型我也还没看
0063 0000000000002040 const [0]: (8)//常量8
006B 04 const type 4//同上上行
006C 02000000 string size (2)//常量名的尺寸
0070 6200 “b”//常量名
const [1]: “b”

//函数列表
* functions:
0072 01000000 sizep (1)//总共有一个内部函数

0076 ** function [0] definition (level 2)//位于序号0 的函数,深度为2(闭包嵌套深度)
** start of function **
0076 00000000 string size (0)//作为了原型的一部分并没有名字字符串
source name: (none)
007A 02000000 line defined (2)//在第二行进行的定义
007E 01 nups (1)//有一个upvalue
007F 01 numparams (1)//有一个参数
0080 00 is_vararg (0)//并不是变长参数函数
0081 02 maxstacksize (2)//栈深度为2

//和顶部函数一样,定义了行号的调试内容
* lines:
0082 04000000 sizelineinfo (4)//总共4个指令

//这4个指令全部都在第2行定义了
[pc] (line)
0086 02000000 [1] (2)
008A 02000000 [2] (2)
008E 02000000 [3] (2)
0092 02000000 [4] (2)

//局部变量列表
* locals:
0096 01000000 sizelocvars (1)//一个局部变量
009A 02000000 string size (2)//名字长度为2
009E 6300 “c”
local [0]: c
00A0 00000000 startpc (0)//作用域从指令0~3
00A4 03000000 endpc (3)

//外部局部变量
* upvalues:
00A8 01000000 sizeupvalues (1)//有一个
00AC 02000000 string size (2)//名字长度为2
00B0 6100 “a”
upvalue [0]: a

//常量列表
* constants:
00B2 01000000 sizek (1)//有一个
00B6 04 const type 4
00B7 02000000 string size (2)//名字为d,长度为2,需要注意的是,d没有加local所以不要把d当成局部变量了,而是全局常量。
00BB 6400 “d”
const [0]: “d”

//内部闭包列表
* functions:
00BD 00000000 sizep (0)//没有闭包了

//具体的指令码
* code:
00C1 04000000 sizecode (4)//总共4个指令
00C5 04000001 [1] getupval 1 0 ; a//获取外部局部变量a
00C9 0C800001 [2] add 1 1 0 //将寄存器1与寄存器0的a进行相加,最后赋值到寄存器1上
00CD 07000001 [3] setglobal 1 0 ; d//将d设置为寄存器1上的值
00D1 1B800000 [4] return 0 1 //返回调用者
** end of function **

//局部闭包结束

//接下来就是顶部函数的指令集了

* code:
00D5 05000000 sizecode (5)//总共5条指令
00D9 01000000 [1] loadk 0 0 ; 8//a在寄存器0上,所以将8赋值上去
00DD 22000001 [2] closure 1 0 ; 1 upvalues//创建一个闭包
00E1 00000000 [3] move 0 0 //move用于管理upvalue
00E5 47000001 [4] setglobal 1 1 ; b//设置全局变量b为刚刚的闭包
00E9 1B800000 [5] return 0 1 //返回闭包退出程序块
** end of function **

//顶部函数结束

00ED ** end of chunk **

以上就是对整个Lua字节码的分析了。

这只是一个相当简单的例子,而且我目前对每一个指令具体的作用也还没有了解,仅仅是为了熟悉整个lua二进制文件的结构。

自己走了一遍下来之后发现已经非常熟练了,如果大家也能自己走一遍过程,其实会发现字节码是非常简单的东西,一点都不会高深莫测。

发表评论

电子邮件地址不会被公开。 必填项已用*标注