Lua Byte Code加載是不是有以下疑問
- 1.Lua字節(jié)碼由哪幾部分組成俩块?
- 2.腳本源代碼對應(yīng)編譯后二進制位置及字節(jié)碼如何加載?
- 3.如何來自定義文件格式?
帶著這些疑問來分析Lua究竟是如何加載的〈亮#基于Lua 5.4.3源碼
一.簡介及實例
Lua腳本編譯二進制Chunk分為:文件頭和函數(shù)塊路狮。所以本文主要從文件頭檢查和函數(shù)塊填充來探索編譯之后的Lua字節(jié)碼(opcode)是怎么給加載起來的?以及二進制如何劃分的蔚约?二進制段位分別代表的含義奄妨?是不是也有類似的疑問。首先我們看如下Lua示例及二進制字節(jié)碼:
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
通過實例并結(jié)合源碼苹祟,將詳細介紹Lua二進制文件頭和函數(shù)塊組織結(jié)構(gòu)及對應(yīng)位的含義砸抛。
二.文件頭
Lua和其他的高級語言一樣,編譯之后會有自己的文件格式來組織二進制數(shù)據(jù)树枫。例如Linux中的ELF文件描述格式是由文件頭直焙、代碼區(qū)、全局?jǐn)?shù)據(jù)區(qū)等組成砂轻。Lua也有自己的文件頭格式(文件類型奔誓、版本號、格式號搔涝、數(shù)據(jù)塊厨喂、指令/數(shù)值size、Lua 整數(shù)/Lua 浮點數(shù))加載的時候交給虛擬機校驗庄呈。二進制塊加載邏輯主要函數(shù)在lundump.c中l(wèi)uaU_undump函數(shù)中杯聚, 我們以lua5.4.3版本為例,luaU_undump是lua加載階段f_parser函數(shù)如果是二進制文件調(diào)用的抒痒,文本文件解釋部分后續(xù)再深入分析,本文接下來重點分析二進制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函數(shù)主要分為兩塊颁褂,文件頭檢查和函數(shù)加載填充故响。我們知道lua編譯成二進制chunk是由兩部分組成:文件頭+函數(shù)塊。
首先對文件頭做檢查颁独,接下來我們進入checkHeader函數(shù)中:
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位寨辩,對應(yīng)二進制塊具體如下:
2.版本號:接下來對版本號作檢查,以官方lua5.4.3版本為例歼冰,對應(yīng)代碼邏輯:
#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靡狞,對應(yīng)二進制塊 16進制就是x54,占用1個字節(jié)隔嫡,說明大版本就是5.4版本:
3.格式號:接下來就是lua里格式號分為官方版本和非官方版本甸怕。0表示官方版本:
#define LUAC_FORMAT 0 /* this is the official format */
if (loadByte(S) != LUAC_FORMAT)
占用1個字節(jié)甘穿,對應(yīng)二進制塊如下:
LUAC_DATA:接下來是LUAC_DATA,用于校驗的數(shù)據(jù)塊用的梢杭。
#define LUAC_DATA "\x19\x93\r\n\x1a\n"
checkliteral(S, LUAC_DATA, "corrupted chunk");
占用6個字節(jié)温兼,\r和\n的十六進制表示分別為0D和0A,對應(yīng)二進制如下:
4.Instruction(unsigned int l_uint32):在文件頭里占用1個字節(jié)武契,04表示4個字節(jié)長度募判。
lua_Integer(long long)、lua_Number(double):在文件頭里占用1個字節(jié)吝羞,08表示8個字節(jié)長度兰伤。分別對這uint32、long long钧排、double三個類型占用的大小作檢查敦腔。在我機器編譯的二進制如下分別占用4、8恨溜、8個字節(jié)大小符衔。
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
二進制對應(yīng)位:
5.接下來是整數(shù)和浮點數(shù):
#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個字節(jié)(lua 5.4.3)的checkHeader部分就全部介紹完了糟袁。
三.函數(shù)體
首先我們通過命令<luac -l -l luac.out>來反編譯出lua的“匯編代碼”判族,可以看出函數(shù)原型的具體信息,比如函數(shù)名项戴、參數(shù)形帮、起至行、指令數(shù)量周叮、常量辩撑、本地變量等,具體含義后面會結(jié)合代碼邏輯詳細分仿耽,大概如下:
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源碼合冀,函數(shù)塊對應(yīng)的具體邏輯如下:
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);
}
根據(jù)上邊的代碼,我們可以大概知道函數(shù)塊的內(nèi)容及加載解釋順序项贺。
函數(shù)塊由upvalue大小君躺、文件名、首行/最后行开缎、參數(shù)個數(shù)棕叫、是否有可變參數(shù)、最大棧大小奕删、字節(jié)碼加載谍珊、常量加載、上值加載、閉包加載砌滞、調(diào)試信息加載等部分組成侮邀。
1.source:表示二進制lua文件名。類型為TString贝润。如下可以看到是長字符串和短字符串绊茧。lua規(guī)定大于40個字符為長字符串,反之為短字符串打掘。這個設(shè)計理解主要應(yīng)該是考慮效率和復(fù)用概率华畏。
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個字節(jié),每組7位尊蚁。高位標(biāo)示后續(xù)是否還有位亡笑,0表示有字節(jié),1表示最后一個字節(jié)横朋。例如:
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表示是否為最后一個字節(jié)仑乌。b = loadByte(S)為88,b & 0x7f(loadInt(S))十進制為8琴锭,所以lua二進制名長度計算為8-1=7個字節(jié)即后面7個字節(jié)就是字符串"@lu.lua"對應(yīng)的字符Ascii碼晰甚。對應(yīng)二進制:40 6c75 2e6c 7561 7個字節(jié):
-
linedefined(80)、lastlinedefined(80) 起始行: loadUnsigned函數(shù)中b & 0x7f = 80 & 0x7f(loadInt(S)) = 10000000 & 01111111 = 0决帖。所以起始行都為0厕九,說明是lua的main函數(shù),lua編譯成二進制后會自動加上main函數(shù)地回。對應(yīng)的二進制如下:
3.numparams:函數(shù)參數(shù)數(shù)量扁远,由“匯編 0+ params” 說明main無參數(shù)所以二進制為00:
4.is_vararg:是否有可變參數(shù),main 有可變參數(shù)刻像,1表示有穿香,二進制01如下:
5.maxstacksize:函數(shù)執(zhí)行過程中需要的虛擬寄存器的大小,由“匯編 2 slots”绎速,說明有2個虛擬寄存器,這里對應(yīng)二進制02:
6.loadCode:函數(shù)執(zhí)行過程中加載具體的二進制指令焙蚓,對應(yīng)源碼loadCode函數(shù):
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對應(yīng)Instruction類型既為具體指令纹冤,f->sizecode為指令的個數(shù),n為可變長整數(shù)购公。由luac -l -l luac.out反編譯之后 main函數(shù)有4條指令或者也可以通過loadInt(S)函數(shù)中 b & 0x7f = 0x84 & 0x7f(loadInt(S)) = 10000100 & 01111111計算得到4萌京,每條指令類型為Instruction 我們知道lua指令由4個字節(jié)32位組成。每條指令對應(yīng)的二進制:
7.loadConstants:加載常量宏浩,對應(yīng)源碼如下:
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個字節(jié)開頭比庄,0x04表示短字符串求妹。字符串長度為長度+1即為2個字節(jié)乏盐。所以對應(yīng)的字節(jié)為:
變長整數(shù)編碼為:0x82, "a"對應(yīng)的是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數(shù)量由"1 upvalue"或者0x81 & 0x7f(loadInt(S)) = 10000001 & 01111111 = 1可以得出由main由1個upvalues制恍,即全局table"_ENV"父能。
對應(yīng)的二進制塊0x81,upvalues結(jié)構(gòu)由name净神、instack何吝、idx、kind成員組成鹃唯,instack爱榕、idx、kind對應(yīng)的二進制0x01坡慌、0x00黔酥、0x00分別占用1個字節(jié):
9.loadProtos: 加載函數(shù)內(nèi)部原型
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);
}
}
內(nèi)部函數(shù)原型數(shù)量由"1 function"或者0x81 & 0x7f (loadInt(S))= 10000001 & 01111111 = 1可以得出由main由1個函數(shù)原型,f->sizepd計算得出為1個內(nèi)部函數(shù)原型。loadProtos內(nèi)部會根據(jù)內(nèi)部函數(shù)原型數(shù)量重新loadFunction重復(fù)執(zhí)行以上各參數(shù)的加載過程八匠。對應(yīng)的二進制為:
對應(yīng)內(nèi)部函數(shù)原型(function <lu.lua:1,4>)二進制絮爷,這里就不詳細分析,和上邊同樣的流程梨树,對應(yīng)的二進制為:
10.loadDebug: 加載調(diào)試消息為二進制最后一塊, 后續(xù)文章會詳細介紹Lua調(diào)試器原理中Proto(函數(shù)原型)調(diào)試信息作用坑夯,此處暫不詳細介紹。
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調(diào)試:由0x84 & 0x7f (loadInt(S))= 10000100 & 01111111 = 4可以得出由main函數(shù)由4個指令組成即對應(yīng)0x01抡四、0x03柜蜈、0xfd、0x03指巡。
a函數(shù):由0x85 & 0x7f (loadInt(S))= 10000101 & 01111111 = 5可以得出由a函數(shù)由5個指令組成即對應(yīng)0x81淑履、0x85、0x5f藻雪、0x45秘噪、0x4e。
到目前為止我們看到2個函數(shù)原型都加載完成了勉耀。為啥最后還有“80 8185 5f45 4e56” 7個自己的數(shù)據(jù)指煎。我們繼續(xù)看loadDebug函數(shù)最后幾行代碼:
n = loadInt(S);
for (i = 0; i < n; i++)
f->upvalues[i].name = loadStringN(S, f);
最后用到upvalues值,其實就是上邊我們提到的全局表“_ENV”
0x5F便斥、0x45至壤、0x4E、0x56表示_ENV枢纠。對應(yīng)的二進制:
至此Lua文件結(jié)構(gòu)及字節(jié)碼加載過程就全部介紹完了像街。
四.總結(jié)
通過探索Lua二進制字節(jié)碼加載整個過程,可以了解到二進制字節(jié)碼文件的組成部分、加載順序镰绎、占用位以及如何加載這些二進制等脓斩。
通過文件頭和函數(shù)塊分析,自定義字節(jié)碼頭部格式甚至函數(shù)塊就應(yīng)該就很清晰了跟狱。
核心文件:lauxlib.c俭厚、lapi.c、ldo.c驶臊、lundump.c
核心函數(shù):lua_load挪挤、f_parser、luaU_undump关翎、checkHeader扛门、loadFunction
通過二進制加載我們是不是有疑問,如何編譯纵寝、解釋Lua二進制及文本文件论寨?總體通過lua提供的luac編譯成字節(jié)碼中間格式然后給到虛擬機解釋執(zhí)行。后面的文章會詳細分析Lua編譯爽茴、解釋原理。
總結(jié)整體流程如下: