Lua Byte Code加载是不是有以下疑问
- 1.Lua字节码由哪几部分组成?
- 2.脚本源代码对应编译后二进制位置及字节码如何加载?
- 3.如何来自定义文件格式?
带着这些疑问来分析Lua究竟是如何加载的。基于Lua 5.4.3源码
一.简介及实例
Lua脚本编译二进制Chunk分为:文件头和函数块。所以本文主要从文件头检查和函数块填充来探索编译之后的Lua字节码(opcode)是怎么给加载起来的?以及二进制如何划分的?二进制段位分别代表的含义?是不是也有类似的疑问。首先我们看如下Lua示例及二进制字节码:
function a()
local x = 1
print(x)
end
编译成二进制如下:
1b4c 7561 5400 1993 0d0a 1a0a 0408 0878
5600 0000 0000 0000 0000 0000 2877 4001
8840 6c75 2e6c 7561 8080 0001 0284 5100
0000 4f00 0000 0f00 0000 4600 0101 8104
8261 8101 0000 8180 8184 0000 0385 0100
0080 8b00 0000 0001 0000 c400 0201 c700
0100 8104 8670 7269 6e74 8100 0000 8085
0101 0000 0180 8182 7881 8581 855f 454e
5684 0103 fd03 8080 8185 5f45 4e56
反编译“汇编”:
main <lu.lua:0,0> (4 instructions at 0x7fcf0ec02910)
0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function
1 [1] VARARGPREP 0
2 [4] CLOSURE 0 0 ; 0x7fcf0ee00780
3 [1] SETTABUP 0 0 0 ; _ENV "a"
4 [4] RETURN 0 1 1 ; 0 out
constants (1) for 0x7fcf0ec02910:
0 S "a"
locals (0) for 0x7fcf0ec02910:
upvalues (1) for 0x7fcf0ec02910:
0 _ENV 1 0
function <lu.lua:1,4> (5 instructions at 0x7fcf0ee00780)
0 params, 3 slots, 1 upvalue, 1 local, 1 constant, 0 functions
1 [2] LOADI 0 1
2 [3] GETTABUP 1 0 0 ; _ENV "print"
3 [3] MOVE 2 0
4 [3] CALL 1 2 1 ; 1 in 0 out
5 [4] RETURN0
constants (1) for 0x7fcf0ee00780:
0 S "print"
locals (1) for 0x7fcf0ee00780:
0 x 2 6
upvalues (1) for 0x7fcf0ee00780:
0 _ENV 0 0
通过实例并结合源码,将详细介绍Lua二进制文件头和函数块组织结构及对应位的含义。
二.文件头
Lua和其他的高级语言一样,编译之后会有自己的文件格式来组织二进制数据。例如Linux中的ELF文件描述格式是由文件头、代码区、全局数据区等组成。Lua也有自己的文件头格式(文件类型、版本号、格式号、数据块、指令/数值size、Lua 整数/Lua 浮点数)加载的时候交给虚拟机校验。二进制块加载逻辑主要函数在lundump.c中luaU_undump函数中, 我们以lua5.4.3版本为例,luaU_undump是lua加载阶段f_parser函数如果是二进制文件调用的,文本文件解释部分后续再深入分析,本文接下来重点分析二进制luaU_undump:
LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) {
LoadState S;
LClosure *cl;
if (*name == '@' || *name == '=')
S.name = name + 1;
else if (*name == LUA_SIGNATURE[0])
S.name = "binary string";
else
S.name = name;
S.L = L;
S.Z = Z;
checkHeader(&S);
cl = luaF_newLclosure(L, loadByte(&S));
setclLvalue2s(L, L->top, cl);
luaD_inctop(L);
cl->p = luaF_newproto(L);
luaC_objbarrier(L, cl, cl->p);
loadFunction(&S, cl->p, NULL);
lua_assert(cl->nupvalues == cl->p->sizeupvalues);
luai_verifycode(L, cl->p);
return cl;
}
我们看到luaU_undump函数主要分为两块,文件头检查和函数加载填充。我们知道lua编译成二进制chunk是由两部分组成:文件头+函数块。
首先对文件头做检查,接下来我们进入checkHeader函数中:
static void checkHeader (LoadState *S) {
/* skip 1st char (already read and checked) */
checkliteral(S, &LUA_SIGNATURE[1], "not a binary chunk");
if (loadByte(S) != LUAC_VERSION)
error(S, "version mismatch");
if (loadByte(S) != LUAC_FORMAT)
error(S, "format mismatch");
checkliteral(S, LUAC_DATA, "corrupted chunk");
checksize(S, Instruction);
checksize(S, lua_Integer);
checksize(S, lua_Number);
if (loadInteger(S) != LUAC_INT)
error(S, "integer format mismatch");
if (loadNumber(S) != LUAC_NUM)
error(S, "float format mismatch");
}
由上边逻辑可以看出文件格式,版本,格式号,指令、整型、浮点型大小等作了检查,总体二进制占位分布如下:
1.文件签名:首先对文件签名信息作检查:
#define LUA_SIGNATURE "\x1bLua"
checkliteral(S, &LUA_SIGNATURE[1], "not a binary chunk");
占用4位,对应二进制块具体如下:
2.版本号:接下来对版本号作检查,以官方lua5.4.3版本为例,对应代码逻辑:
#define LUA_VERSION_MAJOR "5"
#define LUA_VERSION_MINOR "4"
#define LUAC_VERSION (MYINT(LUA_VERSION_MAJOR)*16+MYINT(LUA_VERSION_MINOR))
if (loadByte(S) != LUAC_VERSION)
所以这里5*16+4=84,对应二进制块 16进制就是x54,占用1个字节,说明大版本就是5.4版本:
3.格式号:接下来就是lua里格式号分为官方版本和非官方版本。0表示官方版本:
#define LUAC_FORMAT 0 /* this is the official format */
if (loadByte(S) != LUAC_FORMAT)
占用1个字节,对应二进制块如下:
LUAC_DATA:接下来是LUAC_DATA,用于校验的数据块用的。
#define LUAC_DATA "\x19\x93\r\n\x1a\n"
checkliteral(S, LUAC_DATA, "corrupted chunk");
占用6个字节,\r和\n的十六进制表示分别为0D和0A,对应二进制如下:
4.Instruction(unsigned int l_uint32):在文件头里占用1个字节,04表示4个字节长度。
lua_Integer(long long)、lua_Number(double):在文件头里占用1个字节,08表示8个字节长度。分别对这uint32、long long、double三个类型占用的大小作检查。在我机器编译的二进制如下分别占用4、8、8个字节大小。
typedef l_uint32 Instruction;
/* type of numbers in Lua */
typedef LUA_NUMBER lua_Number; //double
/* type for integer functions */
typedef LUA_INTEGER lua_Integer; // long long
二进制对应位:
5.接下来是整数和浮点数:
#define LUAC_INT 0x5678
#define LUAC_NUM cast_num(370.5)
if (loadInteger(S) != LUAC_INT)
error(S, "integer format mismatch");
if (loadNumber(S) != LUAC_NUM)
error(S, "float format mismatch");
表示为了检测二进制块的大小端方式是否与虚拟机一致。
整型:
浮点型:
至此二进制文件头31个字节(lua 5.4.3)的checkHeader部分就全部介绍完了。
三.函数体
首先我们通过命令<luac -l -l luac.out>来反编译出lua的“汇编代码”,可以看出函数原型的具体信息,比如函数名、参数、起至行、指令数量、常量、本地变量等,具体含义后面会结合代码逻辑详细分,大概如下:
main <lu.lua:0,0> (4 instructions at 0x7f9f515014d0)
0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function
1 [1] VARARGPREP 0
2 [4] CLOSURE 0 0 ; 0x7f9f515015d0
3 [1] SETTABUP 0 0 0 ; _ENV "a"
4 [4] RETURN 0 1 1 ; 0 out
constants (1) for 0x7f9f515014d0:
0 S "a"
locals (0) for 0x7f9f515014d0:
upvalues (1) for 0x7f9f515014d0:
0 _ENV 1 0
function <lu.lua:1,4> (5 instructions at 0x7f9f515015d0)
0 params, 3 slots, 1 upvalue, 1 local, 1 constant, 0 functions
1 [2] LOADI 0 1
2 [3] GETTABUP 1 0 0 ; _ENV "print"
3 [3] MOVE 2 0
4 [3] CALL 1 2 1 ; 1 in 0 out
5 [4] RETURN0
constants (1) for 0x7f9f515015d0:
0 S "print"
locals (1) for 0x7f9f515015d0:
0 x 2 6
upvalues (1) for 0x7f9f515015d0:
0 _ENV 0 0
lua5.4.3源码,函数块对应的具体逻辑如下:
static void loadFunction (LoadState *S, Proto *f, TString *psource) {
f->source = loadStringN(S, f);
if (f->source == NULL) /* no source in dump? */
f->source = psource; /* reuse parent's source */
f->linedefined = loadInt(S);
f->lastlinedefined = loadInt(S);
f->numparams = loadByte(S);
f->is_vararg = loadByte(S);
f->maxstacksize = loadByte(S);
loadCode(S, f);
loadConstants(S, f);
loadUpvalues(S, f);
loadProtos(S, f);
loadDebug(S, f);
}
根据上边的代码,我们可以大概知道函数块的内容及加载解释顺序。
函数块由upvalue大小、文件名、首行/最后行、参数个数、是否有可变参数、最大栈大小、字节码加载、常量加载、上值加载、闭包加载、调试信息加载等部分组成。
1.source:表示二进制lua文件名。类型为TString。如下可以看到是长字符串和短字符串。lua规定大于40个字符为长字符串,反之为短字符串。这个设计理解主要应该是考虑效率和复用概率。
static TString *loadStringN (LoadState *S, Proto *p) {
lua_State *L = S->L;
TString *ts;
size_t size = loadSize(S);
if (size == 0) /* no string? */
return NULL;
else if (--size <= LUAI_MAXSHORTLEN) { /* short string? */
char buff[LUAI_MAXSHORTLEN];
loadVector(S, buff, size); /* load string into buffer */
ts = luaS_newlstr(L, buff, size); /* create string */
}
else { /* long string */
ts = luaS_createlngstrobj(L, size); /* create string */
setsvalue2s(L, L->top, ts); /* anchor it ('loadVector' can GC) */
luaD_inctop(L);
loadVector(S, getstr(ts), size); /* load directly in final place */
L->top--; /* pop string */
}
luaC_objbarrier(L, p, ts);
return ts;
}
loadSize:将24个位分为4个字节,每组7位。高位标示后续是否还有位,0表示有字节,1表示最后一个字节。例如:
data : xxxxxxxx yyyyyyyy zzzzzzzz
step1: 00000xxx 0xxxxxyy 0yyyyyyz 0zzzzzzz
step2: 00000xxx 0xxxxxyy 0yyyyyyz 1zzzzzzz
static size_t loadUnsigned (LoadState *S, size_t limit) {
size_t x = 0;
int b;
limit >>= 7;
do {
b = loadByte(S);
if (x >= limit)
error(S, "integer overflow");
x = (x << 7) | (b & 0x7f);
} while ((b & 0x80) == 0);
return x;
}
b & 0x80表示是否为最后一个字节。b = loadByte(S)为88,b & 0x7f(loadInt(S))十进制为8,所以lua二进制名长度计算为8-1=7个字节即后面7个字节就是字符串"@lu.lua"对应的字符Ascii码。对应二进制:40 6c75 2e6c 7561 7个字节:
-
linedefined(80)、lastlinedefined(80) 起始行: loadUnsigned函数中b & 0x7f = 80 & 0x7f(loadInt(S)) = 10000000 & 01111111 = 0。所以起始行都为0,说明是lua的main函数,lua编译成二进制后会自动加上main函数。对应的二进制如下:
3.numparams:函数参数数量,由“汇编 0+ params” 说明main无参数所以二进制为00:
4.is_vararg:是否有可变参数,main 有可变参数,1表示有,二进制01如下:
5.maxstacksize:函数执行过程中需要的虚拟寄存器的大小,由“汇编 2 slots”,说明有2个虚拟寄存器,这里对应二进制02:
6.loadCode:函数执行过程中加载具体的二进制指令,对应源码loadCode函数:
static void loadCode (LoadState *S, Proto *f) {
int n = loadInt(S);
f->code = luaM_newvectorchecked(S->L, n, Instruction);
f->sizecode = n;
loadVector(S, f->code, n);
}
以上可以看出f->code对应Instruction类型既为具体指令,f->sizecode为指令的个数,n为可变长整数。由luac -l -l luac.out反编译之后 main函数有4条指令或者也可以通过loadInt(S)函数中 b & 0x7f = 0x84 & 0x7f(loadInt(S)) = 10000100 & 01111111计算得到4,每条指令类型为Instruction 我们知道lua指令由4个字节32位组成。每条指令对应的二进制:
7.loadConstants:加载常量,对应源码如下:
static void loadConstants (LoadState *S, Proto *f) {
int i;
int n = loadInt(S);
f->k = luaM_newvectorchecked(S->L, n, TValue);
f->sizek = n;
for (i = 0; i < n; i++)
setnilvalue(&f->k[i]);
for (i = 0; i < n; i++) {
TValue *o = &f->k[i];
int t = loadByte(S);
switch (t) {
case LUA_VNIL:
setnilvalue(o);
break;
case LUA_VFALSE:
setbfvalue(o);
break;
case LUA_VTRUE:
setbtvalue(o);
break;
case LUA_VNUMFLT:
setfltvalue(o, loadNumber(S));
break;
case LUA_VNUMINT:
setivalue(o, loadInteger(S));
break;
case LUA_VSHRSTR:
case LUA_VLNGSTR:
setsvalue2n(S->L, o, loadString(S, f));
break;
default: lua_assert(0);
}
}
}
"1 constant"或者0x81 & 0x7f(loadInt(S)) = 10000001 & 01111111 = 1可以得出由main由1个常量,即常量 "a"。每个常量都以1个字节开头,0x04表示短字符串。字符串长度为长度+1即为2个字节。所以对应的字节为:
变长整数编码为:0x82, "a"对应的是0x61。
8.loadUpvalues: 加载upvalues值(lua中称为上值)
static void loadUpvalues (LoadState *S, Proto *f) {
int i, n;
n = loadInt(S);
f->upvalues = luaM_newvectorchecked(S->L, n, Upvaldesc);
f->sizeupvalues = n;
for (i = 0; i < n; i++) /* make array valid for GC */
f->upvalues[i].name = NULL;
for (i = 0; i < n; i++) { /* following calls can raise errors */
f->upvalues[i].instack = loadByte(S);
f->upvalues[i].idx = loadByte(S);
f->upvalues[i].kind = loadByte(S);
}
}
f->sizeupvalues数量由"1 upvalue"或者0x81 & 0x7f(loadInt(S)) = 10000001 & 01111111 = 1可以得出由main由1个upvalues,即全局table"_ENV"。
对应的二进制块0x81,upvalues结构由name、instack、idx、kind成员组成,instack、idx、kind对应的二进制0x01、0x00、0x00分别占用1个字节:
9.loadProtos: 加载函数内部原型
static void loadProtos (LoadState *S, Proto *f) {
int i;
int n = loadInt(S);
f->p = luaM_newvectorchecked(S->L, n, Proto *);
f->sizep = n;
for (i = 0; i < n; i++)
f->p[i] = NULL;
for (i = 0; i < n; i++) {
f->p[i] = luaF_newproto(S->L);
luaC_objbarrier(S->L, f, f->p[i]);
loadFunction(S, f->p[i], f->source);
}
}
内部函数原型数量由"1 function"或者0x81 & 0x7f (loadInt(S))= 10000001 & 01111111 = 1可以得出由main由1个函数原型,f->sizepd计算得出为1个内部函数原型。loadProtos内部会根据内部函数原型数量重新loadFunction重复执行以上各参数的加载过程。对应的二进制为:
对应内部函数原型(function <lu.lua:1,4>)二进制,这里就不详细分析,和上边同样的流程,对应的二进制为:
10.loadDebug: 加载调试消息为二进制最后一块, 后续文章会详细介绍Lua调试器原理中Proto(函数原型)调试信息作用,此处暂不详细介绍。
static void loadDebug (LoadState *S, Proto *f) {
int i, n;
n = loadInt(S);
f->lineinfo = luaM_newvectorchecked(S->L, n, ls_byte);
f->sizelineinfo = n;
loadVector(S, f->lineinfo, n);
n = loadInt(S);
f->abslineinfo = luaM_newvectorchecked(S->L, n, AbsLineInfo);
f->sizeabslineinfo = n;
for (i = 0; i < n; i++) {
f->abslineinfo[i].pc = loadInt(S);
f->abslineinfo[i].line = loadInt(S);
}
n = loadInt(S);
f->locvars = luaM_newvectorchecked(S->L, n, LocVar);
f->sizelocvars = n;
for (i = 0; i < n; i++)
f->locvars[i].varname = NULL;
for (i = 0; i < n; i++) {
f->locvars[i].varname = loadStringN(S, f);
f->locvars[i].startpc = loadInt(S);
f->locvars[i].endpc = loadInt(S);
}
n = loadInt(S);
for (i = 0; i < n; i++)
f->upvalues[i].name = loadStringN(S, f);
}
main调试:由0x84 & 0x7f (loadInt(S))= 10000100 & 01111111 = 4可以得出由main函数由4个指令组成即对应0x01、0x03、0xfd、0x03。
a函数:由0x85 & 0x7f (loadInt(S))= 10000101 & 01111111 = 5可以得出由a函数由5个指令组成即对应0x81、0x85、0x5f、0x45、0x4e。
到目前为止我们看到2个函数原型都加载完成了。为啥最后还有“80 8185 5f45 4e56” 7个自己的数据。我们继续看loadDebug函数最后几行代码:
n = loadInt(S);
for (i = 0; i < n; i++)
f->upvalues[i].name = loadStringN(S, f);
最后用到upvalues值,其实就是上边我们提到的全局表“_ENV”
0x5F、0x45、0x4E、0x56表示_ENV。对应的二进制:
至此Lua文件结构及字节码加载过程就全部介绍完了。
四.总结
通过探索Lua二进制字节码加载整个过程,可以了解到二进制字节码文件的组成部分、加载顺序、占用位以及如何加载这些二进制等。
通过文件头和函数块分析,自定义字节码头部格式甚至函数块就应该就很清晰了。
核心文件:lauxlib.c、lapi.c、ldo.c、lundump.c
核心函数:lua_load、f_parser、luaU_undump、checkHeader、loadFunction
通过二进制加载我们是不是有疑问,如何编译、解释Lua二进制及文本文件?总体通过lua提供的luac编译成字节码中间格式然后给到虚拟机解释执行。后面的文章会详细分析Lua编译、解释原理。
总结整体流程如下: