最近一两个礼拜都在研究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二进制文件的结构。
自己走了一遍下来之后发现已经非常熟练了,如果大家也能自己走一遍过程,其实会发现字节码是非常简单的东西,一点都不会高深莫测。