Lua字节码文件结构及加载过程

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");
}

由上边逻辑可以看出文件格式,版本,格式号,指令、整型、浮点型大小等作了检查,总体二进制占位分布如下:


image.png

1.文件签名:首先对文件签名信息作检查:

#define LUA_SIGNATURE   "\x1bLua"
checkliteral(S, &LUA_SIGNATURE[1], "not a binary chunk");

占用4位,对应二进制块具体如下:


image.png

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版本:


image.png

3.格式号:接下来就是lua里格式号分为官方版本和非官方版本。0表示官方版本:

#define LUAC_FORMAT 0   /* this is the official format */
if (loadByte(S) != LUAC_FORMAT)

占用1个字节,对应二进制块如下:


image.png

LUAC_DATA:接下来是LUAC_DATA,用于校验的数据块用的。

#define LUAC_DATA   "\x19\x93\r\n\x1a\n"
checkliteral(S, LUAC_DATA, "corrupted chunk");

占用6个字节,\r和\n的十六进制表示分别为0D和0A,对应二进制如下:


image.png

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

二进制对应位:


image.png

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");

表示为了检测二进制块的大小端方式是否与虚拟机一致。
整型:


image.png

浮点型:


image.png

至此二进制文件头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个字节:


image.png

  1. linedefined(80)、lastlinedefined(80) 起始行: loadUnsigned函数中b & 0x7f = 80 & 0x7f(loadInt(S)) = 10000000 & 01111111 = 0。所以起始行都为0,说明是lua的main函数,lua编译成二进制后会自动加上main函数。对应的二进制如下:


    image.png

3.numparams:函数参数数量,由“汇编 0+ params” 说明main无参数所以二进制为00:


image.png

4.is_vararg:是否有可变参数,main 有可变参数,1表示有,二进制01如下:


image.png

5.maxstacksize:函数执行过程中需要的虚拟寄存器的大小,由“汇编 2 slots”,说明有2个虚拟寄存器,这里对应二进制02:


image.png

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位组成。每条指令对应的二进制:


image.png

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个字节。所以对应的字节为:


image.png

变长整数编码为: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个字节:


image.png

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重复执行以上各参数的加载过程。对应的二进制为:


image.png

对应内部函数原型(function <lu.lua:1,4>)二进制,这里就不详细分析,和上边同样的流程,对应的二进制为:


image.png

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。


image.png

a函数:由0x85 & 0x7f (loadInt(S))= 10000101 & 01111111 = 5可以得出由a函数由5个指令组成即对应0x81、0x85、0x5f、0x45、0x4e。


image.png

到目前为止我们看到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。对应的二进制:


image.png

至此Lua文件结构及字节码加载过程就全部介绍完了。

四.总结

通过探索Lua二进制字节码加载整个过程,可以了解到二进制字节码文件的组成部分、加载顺序、占用位以及如何加载这些二进制等。
通过文件头和函数块分析,自定义字节码头部格式甚至函数块就应该就很清晰了。
核心文件:lauxlib.c、lapi.c、ldo.c、lundump.c
核心函数:lua_load、f_parser、luaU_undump、checkHeader、loadFunction
通过二进制加载我们是不是有疑问,如何编译、解释Lua二进制及文本文件?总体通过lua提供的luac编译成字节码中间格式然后给到虚拟机解释执行。后面的文章会详细分析Lua编译、解释原理。
总结整体流程如下:


pro.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容