zoukankan      html  css  js  c++  java
  • 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	"x19x93
    x1a
    "
    

    和 的十六进制表示分别为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)} imes 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:虚拟机、编译器和标准库

  • 相关阅读:
    《计算机网络 自顶向下方法》整理(二)应用层
    《计算机网络 自顶向下方法》整理(一)计算机网络和因特网
    《深入理解C#》整理10-使用async/await进行异步编程
    STM32 HAL库之串口详细篇
    .Net微服务实战之负载均衡(下)
    面试官:来,年轻人!请手撸5种常见限流算法!
    工具用的好,下班回家早!iTerm2使用技巧指北!
    Java编程规范(命名规则)
    Go语言快速安装手册
    Educational Codeforces Round 6 620E. New Year Tree(DFS序+线段树)
  • 原文地址:https://www.cnblogs.com/back-to-the-past/p/15511383.html
Copyright © 2011-2022 走看看