Lua源碼分析(一)二進制塊的加載


Lua對已經編譯過的二進制代碼塊的加載主要集中在luaU_undump這個函數。本篇文章即着重分析該函數的具體實現。本文參考的Lua源碼版本為5.4.0。首先,我們以一個最簡單的lua代碼為例進行編譯:

-- test.lua
print("hello world")

編譯后的二進制代碼塊可以使用UltraEdit等工具進行查看:

接下來,我們將一邊對照二進制塊的具體內容,一邊看代碼:

// lundump.c
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;
}

二進制塊分為頭部和主函數原型兩個部分。Lua首先會對塊的頭部進行檢查,檢查的函數即為checkHeader

// lundump.c
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");
}

首先,Lua會檢查頭部的簽名格式是否合法。Lua二進制塊的簽名是4個字節,用字符串表示為:

// lua.h
/* mark for precompiled code ('<esc>Lua') */
#define LUA_SIGNATURE	"\x1bLua"

這個字符串常量用十六進制表示為0x1B4C7561,這正與我們的截圖相吻合:

檢查過簽名之后,Lua會檢查頭部的版本號是否合法。Lua二進制塊的版本號是1個字節,相關定義如下:

// lua.h
#define LUA_VERSION_MAJOR	"5"
#define LUA_VERSION_MINOR	"4"
// lundump.h
/*
** Encode major-minor version in one byte, one nibble for each
*/
#define MYINT(s)	(s[0]-'0')  /* assume one-digit numerals */
#define LUAC_VERSION	(MYINT(LUA_VERSION_MAJOR)*16+MYINT(LUA_VERSION_MINOR))

可知LUA_VERSION的值為5*16+4=84,寫成十六進制格式為0x54:

后面緊跟着是Lua的格式號,也是占據1個字節,相關定義如下:

// lundump.h
#define LUAC_FORMAT	0	/* this is the official format */

格式號之后是6個字節,叫做LUAC_DATA,定義如下:

// lundump.h
#define LUAC_DATA	"\x19\x93\r\n\x1a\n"

\r和\n的十六進制表示分別為0D和0A,與截圖吻合:

接下來,Lua對二進制塊中Instruction,lua_Integer,lua_Number三種類型所占據的字節大小進行檢查。在我的機器上,它們分別占用4,8,8字節:

// llimits.h
/*
** type for virtual-machine instructions;
** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h)
*/
typedef unsigned int l_uint32;
typedef l_uint32 Instruction;

// luaconf.h
#define LUA_INTEGER		long long
#define LUA_NUMBER	double

// lua.h
/* type for integer functions */
typedef LUA_INTEGER lua_Integer;
/* type of numbers in Lua */
typedef LUA_NUMBER lua_Number;

這里Lua使用checksize函數是一個巧妙的宏:

// lundump.c
#define checksize(S,t)	fchecksize(S,sizeof(t),#t)

static void fchecksize (LoadState *S, size_t size, const char *tname) {
  if (loadByte(S) != size)
    error(S, luaO_pushfstring(S->L, "%s size mismatch", tname));
}

#define后面加上一個#號表示將參數字符串化,將其轉換成一個字符串常量。

接下來,Lua二進制塊存儲了一個整數和浮點數,這是為了檢測二進制塊的大小端方式是否與虛擬機一致。先看整數:

// lundump.h
#define LUAC_INT	0x5678

在我的機器上二進制塊是小端方式。

再看浮點數:

// lundump.h
#define LUAC_NUM	cast_num(370.5)
// llimits.h
#define cast_num(i)	cast(lua_Number, (i))
/* type casts (a macro highlights casts in the code) */
#define cast(t, exp)	((t)(exp))

由上文可知lua_Number其實就是double類型,它是8字節的浮點數,那么問題就轉化為計算370.5的二進制表示,也就是計算用IEEE754表示的結果。復習一下IEEE754:

符號位S顯然為0,然后有:

\[370.5_{(10)} = 101110010.1_{(2)} = 1.011100101_{(2)} \times 2^8 \]

所以指數位E為8+偏移量1023=1031,二進制表示為10000000111。

那么有效數字位M的二進制表示為0111001010000000000000000000000000000000000000000000。

所以最后IEEE754的二進制表示為:

0100000001110111001010000000000000000000000000000000000000000000

轉成十六進制表示為:

4077280000000000

到此為止,函數checkHeader就結束了,接下來的一個字節代表二進制塊中upvalue的數量,lua用來初始化一個closure:

// lundump.c
  cl = luaF_newLclosure(L, loadByte(&S));
// lfunc.h
LUAI_FUNC LClosure *luaF_newLclosure (lua_State *L, int nupvals);

Lua函數原型相關的信息可以使用luac -l -l命令行查看:

可以看出upvalue的數量為1,這也體現在二進制塊上:

然后進入到主函數原型部分,首先看一下函數原型包含的信息,這個可以通過luaF_newproto函數得知:

// lfunc.c
Proto *luaF_newproto (lua_State *L) {
  GCObject *o = luaC_newobj(L, LUA_VPROTO, sizeof(Proto));
  Proto *f = gco2p(o);
  f->k = NULL;
  f->sizek = 0;
  f->p = NULL;
  f->sizep = 0;
  f->code = NULL;
  f->sizecode = 0;
  f->lineinfo = NULL;
  f->sizelineinfo = 0;
  f->abslineinfo = NULL;
  f->sizeabslineinfo = 0;
  f->upvalues = NULL;
  f->sizeupvalues = 0;
  f->numparams = 0;
  f->is_vararg = 0;
  f->maxstacksize = 0;
  f->locvars = NULL;
  f->sizelocvars = 0;
  f->linedefined = 0;
  f->lastlinedefined = 0;
  f->source = NULL;
  return f;
}

填充函數原型的工作主要由loadFunction這個函數完成,它會從二進制塊中依次讀取數據,解析后填充到相應的字段中:

// lundump.c
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);
}

source字段的類型為Lua的TString,它表示編譯二進制塊的源文件名。Lua使用函數loadStringN加載字符串:

// lundump.c
/*
** Load a nullable string into prototype 'p'.
*/
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 */
    loadVector(S, getstr(ts), size);  /* load directly in final place */
  }
  luaC_objbarrier(L, p, ts);
  return ts;
}

函數首先會調用loadSize去加載當前字符串的大小信息,這個大小的值是字符串長度+1,在Lua中是變長整數類型,需要進行解碼:

// lundump.c
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;
}

對於一個整數N,假設它可以用3個字節表示,那么它將會被編碼如下:

data :          xxxxxxxx yyyyyyyy zzzzzzzz
step1: 00000xxx 0xxxxxyy 0yyyyyyz 0zzzzzzz
step2: 00000xxx 0xxxxxyy 0yyyyyyz 1zzzzzzz

第一步就是將這24個比特分為4組,每組7個比特表示數據,最高位的比特表示是否有后續字節,0說明后面還有,1說明這是最后一個字節。對照二進制塊,可知這里的變長整數只有1個字節,因為0x8A & 0x80 = 0x80,最高位的比特為1,那么變長整數的值為0x8A & 0x7F = 10,因此字符串的長度為10-1=9,即后面9個字節就是字符串"@test.lua"對應的字符:

linedefined和lastlinedefined字段是個int,表示函數原型在源文件中的起止行號,它們在二進制塊也以變長整數編碼,解碼可得整數值為0x80 & 0x7F = 0。這是因為Lua規定,main函數的起止行號都是0。

接下來的一個字節代表函數的固定參數個數,main函數沒有固定參數,因此numparams這個字段值為0。

再往下的一個字節表示函數是否有變長參數,而main函數是有變長參數的,因此is_vararg字段值為1。

maxstacksize字段表示函數執行期間需要的虛擬寄存器數量。該字段的值可以通過前面提到的使用luac -l -l命令行列出的slots得到,這里slots的值為2。

然后Lua調用函數loadCode來加載具體的指令信息:

// lundump.c
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);
}

首先是一個變長整數編碼表示當前函數指令的數量,我們用luac查看可知指令數量為5條,因此對應二進制塊的值是0x85;每條指令又是Instruction類型,該類型我們之前提到過它其實就是unsigned int,因此對應到二進制塊就是緊跟指令數量之后的5×4=20字節都是指令的內容。

指令之后是常量信息。Lua使用函數loadConstants加載常量:

// lundump.c
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);
    }
  }
}

用luac查看可知常量數量為2,即"print"和"hello world",因此對應二進制塊的值是0x82。每個常量都以1個字節打頭,標識其類型,例如這里的0x04表示類型為短字符串,而前面提到過短字符串由表示其長度+1的變長整數編碼和字符串的字符內容組成,對於"print",變長整數編碼為0x86,對於"hello world",則是0x8C。

常量之后則是upvalues的信息,Lua使用函數loadUpvalues加載:

// lundump.c
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++) {
    f->upvalues[i].name = NULL;
    f->upvalues[i].instack = loadByte(S);
    f->upvalues[i].idx = loadByte(S);
    f->upvalues[i].kind = loadByte(S);
  }
}

用luac查看可知upvalues數量為1,因此對應二進制塊的值是0x81。upvalues有name,instack,idx,kind四個屬性,其中后面三個屬性來自接下來的3個字節。

upvalues之后是子函數原型。這里的lua只是簡單地print了一下"hello world",因此子函數原型長度為0,即對應二進制塊的值為0x80。Lua調用loadProtos加載子函數原型,可以發現函數內部遞歸調用了loadFunction處理加載過程:

// lundump.c
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);
  }
}

二進制塊的最后是一些調試信息。Lua使用loadDebug函數加載調試信息:

// lundump.c
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);
}

首先是行號信息,分為相對行號lineinfo和絕對行號abslineinfo。sizelineinfo是表示相對行號lineinfo長度的變長整數字段,lineinfo中存儲了相對於上一條指令的行號偏移,每個偏移量用一個字節表示。如果一個字節無法表示行號,則還會使用abslineinfo記錄絕對行號。我們這里源代碼只有1行,因此用相對行號就足以表達所有指令的行號,由luac可知一共有5條指令,行號都是1,那么轉換成偏移表示就是0x01 0x00 0x00 0x00 0x00。這里沒有用到abslineinfo,因此sizeabslineinfo為0:

然后是局部變量的調試信息和upvalues的調試信息,由luac可知當前並沒有用到局部變量,因此sizelocvars為0。upvalues的數量為1,因此還要進一步讀取它的name字段"_ENV",該字段是一個短字符串,那么二進制塊表示為0x85(表示長度+1) 0x5F 0x45 0x4E 0x56:

自此,Lua 5.4.0的二進制塊的加載就分析完了。

如果你覺得我的文章有幫助,歡迎關注我的微信公眾號(大齡社畜的游戲開發之路

reference

[1] Lua 5.4之二進制塊格式

[2] 自己動手實現Lua:虛擬機、編譯器和標准庫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM