lua5.1字节码解析

字节码文件头

在这里插入图片描述lua5.1字节码文件头的长度为12字节,在我的环境里(Win7 64位,VS下编译为Win32应用)如下:
1b4c 7561 5100 0104 0404 0800
其中第1-4字节为:"\033Lua";
第5字节标识lua的版本号,lua5.1为 0x51;
第6字节为官方中保留,lua5.1中为 0x0;
第7字节标识字节序,little-endian为0x01,big-endian为0x00;
第8字节为sizeof(int);
第9字节为sizeof(size_t);
第10字节为sizeof(Instruction),Instruction为lua内的指令类型,在32位以上的机器上为unsigned int;
第11字节为sizeof(lua_Number),lua_Number即为double;
第12字节是判断lua_Number类型起否有效,一般为 0x00;

lua5.1 在文件头之后,就是正头,它由一个个函数组成,其中第一个函数包含由文件内全部内容,引全局函数名为"@"+文件件名(包含".lua"后缀),在此文件中定义的函数都会在全局函数中以常量字符串保存;
每个函数的内容缓存如下:

源文件名的长度(包括'\0'),为sizeof(size_t)个字节长,只有全局函数有源文件名,其它内部函数其长度填0;
源文件名(包括\0),长度为长度*sizeof(char)个字节;
函数行数,全局函数的填0,长度为sizeof(int)个字节;
函数的最后一行,全局函数的填0,长度为sizeof(int)个字节;
函数的upvalues数目,长度为sizeof(char)个字节;
函数的参数个数,全局函数的填0,长度为sizeof(char)个字节;
函数的vararg个数,只有全局函数有;
函数最大的栈数目,长度为sizeof(char)个字节;
函数的指令数目,长度为sizeof(int)个字节;
函数的指令,长度为指令数目*sizeof(Instruction)个字节;
函数中常量的数目,长度为sizeof(int)个字节;
函数中的常量,长度为常量数目*(常量类似标识长度+指定常量点用的长度),常量类似标识长度为sizeof(char)个字节;
函数中的内部函数数目,长度为sizeof(int)个字节;
内部函数的定,格式同外部函数;
函数的调试信息;

正文

源码test.lua

local a = 18
local b = 19
local c = 20
local d = 21
local function test001()
	print("caizujun")
end
print("zhangman")

生成字节码

luac.exe -o test.out test.lua

test.out 字节码如下
在这里插入图片描述
解析字节码

luac.exe -l test.out

在这里插入图片描述
在字节码文件中进行标注:如下

在这里插入图片描述

分析的过程如下
1:12个字节来描述字节码头:
2:4个字节来描述当前文件名的长度,包含(.lua/0)
3:文件名的具体字符串,长度为第2步中算出的长度,每一个文件都是一个全局函数
4:函数在源码中的起始行号,四个字节,全局函数为00 00 00 00
5:函数在源码中的结束行号,四个字节,全局函数为00 00 00 00
5.5:这里需要注意的是,如果是全局函数,全局函数就是指当前的文件,那么不需要加四个字节(00 00 00 00),表示函数的解析开始,否则文件里的所有函数在进行到这一步时都要加四个字节(00 00 00 00),来表示函数的开始
6:函数的配置,占四个字节,分别是函数的upvalue个数,函数的params个数,函数的vararg个数(只有全局函数有),函数的栈size
7: 函数的指令数目,占4个字节
8:具体的指令,一个一个都列出来,每一条指令占四个字节
9:常量的数目,占四个字节
10:列出每一个常量,常量的结构就是:(类型+常量值),(类型+字符串长度+常量值)
类型就使用一个字节表示,字符串长度使用4个字节表示
0x03:表示number,后边直接加上number 值
0x04:表示string,后边加上字符串长度,再加字符串值
11:紧跟常量分析完以后,又会使用四个字节来判断当前函数拥有的子函数数量,
如果当前拥有的子函数数量是:00 00 00 00,表示0个子函数,那么就要开始计算当前函数的每一条操作指令,在源码中对应的行号,继续往下走,即进行到第12步
如果当前拥有的子函数数量大于0,则会跳转到第5步

12:指令的长度,使用四个字节表示
13:所有指令,在源码中对应的行号,每一个行号使用4个字节来表示
格外注意:我们的一条源码可能会被解析成多条指令,所以可能会出现多条指令对应的行号相同
14:所有的local 变量的数量,使用四个字节来表示
如果local 变量的数量等于00 00 00 00,则表明没有,则直接跳转到16步
如果local变量的数量大于0,则继续往下走
15:每一个local 变量都是一个字符串,所以他的表示就是按照顺序,字符串的长度+字符串的值,字符串的长度使用四个字节,所以一个local变量所占的字节数=4+字符串长度
16:在函数的末尾+00 00 00 00,表示当前函数分析完

lua语言在字节码上的执行过程

lua源码

print("zhangman")

lua字节码
在这里插入图片描述
打印这个lua字节码里面存放的信息
在这里插入图片描述
第一个操作码指令:GETGLOBAL
lvm.c
下面这句代码的意思就是去当前的闭包环境中去寻找一个key,这个闭包环境就是一个table,key被存储在指令的rb中,
找到table[key]以后将结果存储到ra寄存器中

  case OP_GETGLOBAL: {
		 /*
		    通过key 获取全局变量;
			printf("---%s----\n",svalue(rb));
		 */
        TValue g;
        TValue *rb = KBx(i);
        sethvalue(L, &g, cl->env);
        lua_assert(ttisstring(rb));
        Protect(luaV_gettable(L, &g, rb, ra));
        continue;
      }

第二个操作码指令:LOADK
下面的意思就是将存储在rb中的常量赋值到ra寄存器中
lvm.c

case OP_MOVE: {
		  /*将RB(i)的值移动寄存器RA(i)中;
		  该操作为赋值,另外函数调用也会生成一条move指令 
		  形如 local a = 10;local b = a;这句话的意思就是有一个寄存器a来存放常量10,又生成一个寄存器b,需要把寄存器a的值move到寄存器b中
		  */
        setobjs2s(L, ra, RB(i));
        continue;
      }

第三个操作码指令:CALL
下面的代码就是执行函数
lvm.c

case OP_CALL: {
		/*
		    执行函数
		    printf("-calll----%d---%d----%p----%p----%p\n", nresults, b,ra+1,ra+2,ra+3);
			对于lua中函数栈的理解:通过打印地址发现,其实lua中根本不存在栈的结构,这个东西是基于虚拟机的寄存器抽象出来的
			一个函数的调用:通常是函数的首地址和若干个参数地址和若干个返回值,然后调用luaD_precall执行
			这个函数的首地址就是这个ra寄存器地址,然后至于这些若干个参数地址,也是在执行这个OP_CALL操作码之前
			就已经确定了的,这些若干个的参数的地址的生成是从左到右依次从ra这个地址往上加的,当然这个生成是在语法分析阶段
			就已经完成了的,然后将最后一个参数地址+1赋值给L->top,这样好像就是栈顶指向了一个自由的元素,然后栈顶指针在具体的函数操作中
			通过++或者--来模仿栈的进栈和出栈,所以可以理解为栈
		*/
		/*
		  获取函数参数个数(b-1)
		*/
        int b = GETARG_B(i);
		/*
		  获取函数的返回值要赋值给几个值
		  local test = function() return 1,2 end ;local p1,p2 = test();
		*/
        int nresults = GETARG_C(i) - 1;
		//将栈的栈顶指针指向最上面一个空闲位置
		/*
		这个ra寄存器里的地址是一个函数的首地址
		当执行到这个操作码的时候,说明参数已经全部入栈了,这里只是没有看到入栈过程
		我们发现当一个函数被调用之前,会先进行load常量,最后move,其实这样做就相当于入栈了
		ra寄存器的分配是在生成字节码的过程就已经决定了的
		ra+b----null<----------L->top
		......
		ra+3----参数3<----------OP_LOADK
		ra+2----参数2<----------OP_LOADK
		ra+1----参数1<----------OP_LOADK
		ra----->栈底<-----------L->base
		*/
        if (b != 0) L->top = ra+b;  /* else previous instruction set top */
        L->savedpc = pc;
        switch (luaD_precall(L, ra, nresults)) {
          case PCRLUA: {
            nexeccalls++;
            goto reentry;  /* restart luaV_execute over new Lua function */
          }
          case PCRC: {
            /* it was a C function (`precall' called it); adjust results */
            if (nresults >= 0) L->top = L->ci->top;
            base = L->base;
            continue;
          }
          default: {
            return;  /* yield */
          }
        }
      }

最后一条操作码指令:RETURN

 case OP_RETURN: {
		/*
		  一个文件,一个函数的结尾,都会加一个return指令
		*/
		
        int b = GETARG_B(i);
        if (b != 0) L->top = ra+b-1;
        if (L->openupval) luaF_close(L, base);
        L->savedpc = pc;
        b = luaD_poscall(L, ra);
		if (--nexeccalls == 0)  /* was previous function running `here'? */
		{
			
			for (int i = 0; i < codeCount; i++)
			{
				printf("---%s-----%f\n", opcodeName[i],opcodeStr[i]);
			}
			printf("end   *********************\n");
			
			/*停止机器码执行 */
			return;  /* no: return */
		}
        else {  /* yes: continue its execution */
          if (b) L->top = L->ci->top;
          lua_assert(isLua(L->ci));
          lua_assert(GET_OPCODE(*((L->ci)->savedpc - 1)) == OP_CALL);
		  //printf("continue   ********************\n");
          goto reentry;
		  
        }
      }

对第三条操作码继续深究
lvm.c
在栈上准备好数据以后,进入到luaD_precall这个函数

switch (luaD_precall(L, ra, nresults)) {
          case PCRLUA: {
            nexeccalls++;
            goto reentry;  /* restart luaV_execute over new Lua function */
          }
          case PCRC: {
            /* it was a C function (`precall' called it); adjust results */
            if (nresults >= 0) L->top = L->ci->top;
            base = L->base;
            continue;
          }
          default: {
            return;  /* yield */
          }
        }

ldo.c
下面这个函数其实非常重要,进行了一次判断,是否为lua函数或者为c函数
很明显print这是一个c函数
注意看这语句 n = (curr_func(L)->c.f)(L); / do the actual call */
打一下断点就会跳到注册的c函数了

int luaD_precall (lua_State *L, StkId func, int nresults) {
  LClosure *cl;
  ptrdiff_t funcr;
  if (!ttisfunction(func)) /* `func' is not a function? */
    func = tryfuncTM(L, func);  /* check the `function' tag method */
  funcr = savestack(L, func);
  cl = &clvalue(func)->l;
  L->ci->savedpc = L->savedpc;
  if (!cl->isC) {  /* Lua function? prepare its call */
	/*如果是lua函数 则做执行前的准备*/
    CallInfo *ci;
    StkId st, base;
    Proto *p = cl->p;
    luaD_checkstack(L, p->maxstacksize);
    func = restorestack(L, funcr);
    if (!p->is_vararg) {  /* no varargs? */
      base = func + 1;
      if (L->top > base + p->numparams)
        L->top = base + p->numparams;
    }
    else {  /* vararg function */
      int nargs = cast_int(L->top - func) - 1;
      base = adjust_varargs(L, p, nargs);
      func = restorestack(L, funcr);  /* previous call may change the stack */
    }
    ci = inc_ci(L);  /* now `enter' new function */
    ci->func = func;
    L->base = ci->base = base;
    ci->top = L->base + p->maxstacksize;
    lua_assert(ci->top <= L->stack_last);
    L->savedpc = p->code;  /* starting point */
    ci->tailcalls = 0;
    ci->nresults = nresults;
    for (st = L->top; st < ci->top; st++)
      setnilvalue(st);
    L->top = ci->top;
    if (L->hookmask & LUA_MASKCALL) {
      L->savedpc++;  /* hooks assume 'pc' is already incremented */
      luaD_callhook(L, LUA_HOOKCALL, -1);
      L->savedpc--;  /* correct 'pc' */
    }
    return PCRLUA;
  }
  else {  /* if is a C function, call it */
	/*如果是c函数 则直接执行*/
    CallInfo *ci;
    int n;
    luaD_checkstack(L, LUA_MINSTACK);  /* ensure minimum stack size */
    ci = inc_ci(L);  /* now `enter' new function */
    ci->func = restorestack(L, funcr);
    L->base = ci->base = ci->func + 1;
    ci->top = L->top + LUA_MINSTACK;
    lua_assert(ci->top <= L->stack_last);
    ci->nresults = nresults;
    if (L->hookmask & LUA_MASKCALL)
      luaD_callhook(L, LUA_HOOKCALL, -1);
    lua_unlock(L);
    n = (*curr_func(L)->c.f)(L);  /* do the actual call */
    lua_lock(L);
    if (n < 0)  /* yielding? */
      return PCRYIELD;
    else {
      luaD_poscall(L, L->top - n);
      return PCRC;
    }
  }
}

进入到c函数
此时栈中的情况时

L->top-------null
l->top-1-----zhangman
L->base------func

经过下面这个函数的处理:

lua_getglobal(L, "tostring");
L->top-------null
l->top-1-----tostringFUNC 这也是一个c函数
l->top-2-----zhangman
L->base------func

lua_pushvalue(L, -1);  /* function to be called */
lua_pushvalue(L, i);   /* value to print */
lua_call(L, 1, 1);
L->top-------null
l->top-1-----zhangman
l->top-2-----tostringFUNC 这也是一个c函数
l->top-3-----tostringFUNC 这也是一个c函数
l->top-4-----zhangman
L->base------func

最后向控制台输出
s = lua_tostring(L, -1);  /* get result */
fputs(s, stdout);
最后我们在屏幕上看到输出zhangma

lbaselib.c

/*
** If your system does not support `stdout', you can just remove this function.
** If you need, you can define your own `print' function, following this
** model but changing `fputs' to put the strings at a proper place
** (a console window or a log file, for instance).
*/
static int luaB_print (lua_State *L) {
  int n = lua_gettop(L);  /* number of arguments */
  int i;
  lua_getglobal(L, "tostring");
  for (i=1; i<=n; i++) {
    const char *s;
    lua_pushvalue(L, -1);  /* function to be called */
    lua_pushvalue(L, i);   /* value to print */
    lua_call(L, 1, 1);
    s = lua_tostring(L, -1);  /* get result */
    if (s == NULL)
      return luaL_error(L, LUA_QL("tostring") " must return a string to "
                           LUA_QL("print"));
    if (i>1) fputs("\t", stdout);
    fputs(s, stdout);
    lua_pop(L, 1);  /* pop result */
  }
  fputs("\n", stdout);
  return 0;
}
static int luaB_tostring (lua_State *L) {
  luaL_checkany(L, 1);
  if (luaL_callmeta(L, 1, "__tostring"))  /* is there a metafield? */
    return 1;  /* use its value */
  switch (lua_type(L, 1)) {
    case LUA_TNUMBER:
      lua_pushstring(L, lua_tostring(L, 1));
      break;
    case LUA_TSTRING:
      lua_pushvalue(L, 1);
      break;
    case LUA_TBOOLEAN:
      lua_pushstring(L, (lua_toboolean(L, 1) ? "true" : "false"));
      break;
    case LUA_TNIL:
      lua_pushliteral(L, "nil");
      break;
    default:
      lua_pushfstring(L, "%s: %p", luaL_typename(L, 1), lua_topointer(L, 1));
      break;
  }
  return 1;
}

总结:我觉得有一个特别重要的信息就是每条指令的ra寄存器的地址分配
取iABC这种模式来说吧,每条指令的长度都是32位,也就是4个字节
位数 — 6 8 9 9
指令1:i A B C
指令2:i A B C
指令3:i A B C

指令N:i A B C
在虚拟机中,我们首先根据操作码可以获取到模式,再根据模式,我们可以使用指定函数获取到模式中上面这四个位置的数据是多少,在字节码生成阶段,ra寄存器的地址分配就已经确定了的,你比如说上面的例子中,我们执行一条lua语句生成了四条指令,我们惊奇的发现,在前三条语句中,都是在往ra寄存器里填值,最后让栈底指针指向函数首地址,让栈顶指针去指向空,栈的大小就是函数的参数+函数的首地址,但在前两条语句中没有出现任何的栈操作,所以ra寄存器一定是有关联的,且是依次递加
----ra寄存器----
【0x00000001】1
【0x00000002】2
【0x00000003】3

【0x0000000N】4
注意:我们发现ra寄存器只有8位,随便一个整数都有可能超过8位,更别说字符串了,所以ra寄存器里存的是偏移地址,你可以这么理解:数据就好像书一样被存放到了一个书架上,每一格挡的空间是固定的,这里存放你的书,但是格挡有编号,我们的ra寄存器就是存的这个编号,系统会根据这个编号来找到对应的书也就是存储的真实数据
函数的调用模式:我们的字节码中埋藏了ra寄存器埋藏了偏移地址,在函数调用前,每一条指令都有一个ra寄存器,每一条指令中也都埋藏了参数数据的偏移地址,先执行loadk指令,就是将参数一个一个的放入ra寄存器中,最后执行call命令,就是将函数首地址的偏移地址存入ra寄存器中,最后调整栈,让栈顶指向空元素,让栈底指针指向函数的首地址,执行函数,这个函数执行栈就是在字节码生成过程中就已经确定了的

拐弯

问一个问题:难道所有的指令都是逐步推进的吗?原则上是这样的,但是也有分叉的时候,比如调用lua函数,

虚拟机主逻辑:
可以看到我们的指令依靠这个savedpc来做记录
执行就是靠pc++往后执行指令

void luaV_execute (lua_State *L, int nexeccalls) {
  LClosure *cl;
  StkId base;
  TValue *k;
  float opcodeStr[10000];
  char *opcodeName[10000];
  int codeCount = 0;
  const Instruction *pc;
  //printf("进入虚拟机执行**************************\n");
  reentry:  /* entry point */
  /*
  当前处理的函数必须是lua函数
  */
  lua_assert(isLua(L->ci));
  /*
  初始化操作码指令的位置
  */
  pc = L->savedpc;
  ......

下面的代码就是现场的更换和复原

//OP_CALL指令会触发下面这个函数
//下面这个函数就是现场更换
luaD_precall(){
......
if (!cl->isC) {  /* Lua function? prepare its call */
   ......
    ci = inc_ci(L);  /* now `enter' new function ci++ */ 
    ci->func = func;
    L->base = ci->base = base;
    ci->top = L->base + p->maxstacksize;
    lua_assert(ci->top <= L->stack_last);
    L->savedpc = p->code;  /* starting point */
	printf("code------%p-------------\n", p->code);
    ci->tailcalls = 0;
    ci->nresults = nresults;
   ......
}
......
}

//每一个函数执行的最后都会调用一个return操作码指令,而这个指令会触发下面这个函数
//下面这个函数就是现场复原
int luaD_poscall (lua_State *L, StkId firstResult) {
  StkId res;
  int wanted, i;
  CallInfo *ci;
  if (L->hookmask & LUA_MASKRET)
    firstResult = callrethooks(L, firstResult);
  ci = L->ci--;
  res = ci->func;  /* res == final position of 1st result */
  wanted = ci->nresults;
  L->base = (ci - 1)->base;  /* restore base */
  L->savedpc = (ci - 1)->savedpc;  /* restore savedpc */
  /* move results to correct place */
  for (i = wanted; i != 0 && firstResult < L->top; i--)
    setobjs2s(L, res++, firstResult++);
  while (i-- > 0)
    setnilvalue(res++);
  L->top = res;
  return (wanted - LUA_MULTRET);  /* 0 iff wanted == LUA_MULTRET */
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值