lua执行字节码的过程介绍

前面一篇文章中介绍了lua给下面代码生成最终的字节码的整个过程,这次我们来看看lua vm执行这些字节码的过程。

1 foo = "bar"
2 local a, b = "a", "b"
3 foo = a

生成的字节码如下所示:

之前lua是在luaY_parser函数(入口)中完成了lua脚本的解析生成字节码的整个过程的,在生成了main func(过程见“lua解析赋值类型代码的过程“)后luaY_parser会返回一个Proto结构体指针tf,Proto结构将描述整个main func的所有信息。

 1 //如果此字符是LUA_SIGNATURE中的第一个字符说明文件内容是预编译好的文件内容,因此利用函数luaU_undump来加载一个预编译后的代码块
 2   //否则是未编译的脚本源码,利用luaY_parser来对源码进行parse
 3   tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z,
 4                                                              &p->buff, p->name);
 5   cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
 6   cl->l.p = tf;
 7   for (i = 0; i < tf->nups; i++)  /* initialize eventual upvalues */
 8     cl->l.upvals[i] = luaF_newupval(L);
 9   setclvalue(L, L->top, cl);
10   incr_top(L);

接下来第5行,函数luaF_newLclosure生成了一个Closure结构体来表示lua的closure,然后下一行将Proto结构体地址传给cl保存,接下来的循环里cl的upvalue数组记录下main func中的upvalue,然后setclvalue函数将cl放入到lua stack的栈顶上,incr_top将栈顶L->top加一。此时lua stack的顶部存放了包裹了main func的closure结构体,下面lua将会调用lua_pcall函数来执行这个closure了,也即vm加载整个生成的字节码并加以解释。

 1 LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) {
 2   struct CallS c;
 3   //... ...
 4   c.func = L->top - (nargs+1);  /* function to be called */
 5   c.nresults = nresults;
 6   status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
 7   //... ...
 8 }
 9 /*
10 ** Execute a protected call.
11 */
12 struct CallS {  /* data to `f_call‘ */
13   StkId func;
14   int nresults;
15 };

首先第4行根据要执行的函数参数数量和L->top的值来算出function在lua stack中的位置并将其保存到CallS结构体中,其中CallS结构体中的StkId类型为stack下标类型。接着第6行将c和f_call函数一起传入luaD_pcall函数中,luaD_pcall函数执行一些标志的设置后调用函数luaD_rawrunprotected,函数luaD_rawrunprotected内部调用f_call并将c作为其参数传入。如下所示:

1 static void f_call (lua_State *L, void *ud) {
2   struct CallS *c = cast(struct CallS *, ud);
3   luaD_call(L, c->func, c->nresults);
4 }

在luaD_call中首先判断lua此时是否到达了函数调用层次的最大值,超过这报错否则判断要执行的函数是不是lua function,是的话就调用luaV_execute函数来运行vm执行字节码。

 1 void luaD_call (lua_State *L, StkId func, int nResults) {
 2   if (++L->nCcalls >= LUAI_MAXCCALLS) {
 3     if (L->nCcalls == LUAI_MAXCCALLS)
 4       luaG_runerror(L, "C stack overflow");
 5     else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3)))
 6       luaD_throw(L, LUA_ERRERR);  /* error while handing stack error */
 7   }
 8   if (luaD_precall(L, func, nResults) == PCRLUA)  /* is a Lua function? */
 9     luaV_execute(L, 1);  /* call it */
10   L->nCcalls--;
11   luaC_checkGC(L);
12 }

luaV_execute函数是vm执行字节码的核心过程,整个函数约有400行代码,由于整个过程分支太多,我们只讲解示例中的字节码解析过程。

 1 void luaV_execute (lua_State *L, int nexeccalls) {
 2   LClosure *cl;
 3   StkId base;
 4   TValue *k;
 5   const Instruction *pc;
 6  reentry:  /* entry point */
 7   lua_assert(isLua(L->ci));
 8   pc = L->savedpc;
 9   cl = &clvalue(L->ci->func)->l;
10   base = L->base;
11   k = cl->p->k;
12 //... ...

L->savedpc为字节码数组的指针,因此pc保存了当前要执行字节码的下标,clvalue萃取出当前要执行的lua function对应的closure,k指向了当前function的常量数组。

下面先来看看vm解释loadk01字节码的过程。

 1 /* main loop of interpreter */
 2   for (;;) {
 3     const Instruction i = *pc++;
 4     StkId ra;
 5     //... ...
 6     ra = RA(i);
 7     //... ...
 8     switch (GET_OPCODE(i)) {
 9       //... ...
10       case OP_LOADK: {
11         setobj2s(L, ra, KBx(i));
12         continue;
13       }
14 //... ...

第3行i保存了当前要执行的字节码,同时pc指向下一条字节码,第6行ra保存了通过宏RA萃取出的字节码中的a部分并与function stack的base相加得出的stack中的值;第8行Get_OPCODE宏萃取出字节码i的类型,结果是OP_LOADK,因此调用了setobj2s函数,其中KBx宏萃取出字节码i的bx部分并与function的常量数组地址相加得出的常量值,这里ra指向了function stack中相应的位置,KBx(i)部分指向了当前function中常量数组中存放的常量“bar”。

1 /* from stack to (same) stack */
2 #define setobjs2s    setobj
3 /* to stack (not from same stack) */
4 #define setobj2s    setobj
5
6 #define setobj(L,obj1,obj2) 7   { const TValue *o2=(obj2); TValue *o1=(obj1); 8     o1->value = o2->value; o1->tt=o2->tt; 9     checkliveness(G(L),o1); }

obj1为ra,obj2为KBx结果。可以看到第7行将这两个值转换为了TValue,并将o2的value设为o1的value,o2的值的类型设为o1的类型,效果上完成了将“bar”的值存放在了function stack上。接着又返回到上面的主循环处读取下一个字节码并执行,下一个要执行的字节码为setglobal00.

 1 switch (GET_OPCODE(i)) {
 2       //... ...
 3       case OP_SETGLOBAL: {
 4         TValue g;
 5         sethvalue(L, &g, cl->env);
 6         lua_assert(ttisstring(KBx(i)));
 7         Protect(luaV_settable(L, &g, KBx(i), ra));
 8         continue;
 9       }
10 //... ...

首先第5行中,cl->env为当前function的环境,函数sethvalue将其传给了g,KBx(i)指向了function常量数组中的值,ra为stack中的值,这里为前一条字节码loadk保存在stack中的“bar”。

 1 void luaV_settable (lua_State *L, const TValue *t, TValue *key, StkId val) {
 2   int loop;
 3   TValue temp;
 4   for (loop = 0; loop < MAXTAGLOOP; loop++) {
 5     const TValue *tm;
 6     if (ttistable(t)) {  /* `t‘ is a table? */
 7       Table *h = hvalue(t);
 8       TValue *oldval = luaH_set(L, h, key); /* do a primitive set */
 9       if (!ttisnil(oldval) ||  /* result is no nil? */
10           (tm = fasttm(L, h->metatable, TM_NEWINDEX)) == NULL) { /* or no TM? */
11         setobj2t(L, oldval, val);
12         h->flags = 0;
13         luaC_barriert(L, h, val);
14         return;
15       }
16  //... ...
17 }

这里首先判断g是不是table,然后第7行取得g的hash部分,通过第8行luaH_set里的luaH_get得到table中key对应的old value。最后第11行,函数setobj2t将val("bar")存放在了全局变量foo的位置处,即foo = “bar”。

1 #define setobj(L,obj1,obj2) 2   { const TValue *o2=(obj2); TValue *o1=(obj1); 3     o1->value = o2->value; o1->tt=o2->tt; 4     checkliveness(G(L),o1); }

好了到了这里语句foo = “bar”对应的两条字节码的解释过程已经全部介绍完了,下面的三条字节码就不再详细解释了,大家可以按照上面的路线自己过一遍~

时间: 2024-10-09 13:58:34

lua执行字节码的过程介绍的相关文章

【分析】dalvik虚拟机解释执行字节码

参考源码版本:Android-4.4.4_r2 提示:大部分分析直接注释在代码内. dvmInterpret函数中调用了dvmInterpretPortable函数对方法的字节码进行解释执行,dvmInterpret在dalvik/vm/interp/Interp.cpp文件中. dvmInterpretPortable函数在dalvik/vm/mterp/out/InterpC-portable.cpp文件中. 使用gcc -E -P -C InterpC-portable.cpp > Int

重读《深入理解Java虚拟机》五、虚拟机如何执行字节码?虚拟机执行引擎的工作机制

Class文件二进制字符流通过类加载器和虚拟机加载到内存(方法区)完成在内存上的布局和初始化后,虚拟机字节码执行引擎就可以执行相关代码实现程序所定义的功能.虚拟机执行引擎执行的对象是方法(均特指非本地方法),方法是 着一个程序所定义的一个功能的载体,实现预定的业务功能或者特定的功能等. Java虚拟机内存内针对方法的执行专门划分了一个区域即虚拟机栈.虚拟机栈内通过栈帧结构来存储调用方法和执行方法需要的局部变量,操作数栈.方法返回值等,通过栈帧的出入栈来表示方法的执行顺序. 1.栈帧结构:虚拟机内

虚拟机字节码执行引擎

在前面的几篇文章里,从Java虚拟机内存结构开始,经历了虚拟机垃圾收集机制.Class类文件结构到后来的虚拟机类加载机制,一步步的进入到了Java虚拟机即Java底层的世界.在有了前面的基础之后,接下来就应该进入Java虚拟机最重要的部分了--虚拟机字节码执行引擎,毕竟,这是Java程序得以在不同机器上运行的核心部分. Java是通过实现Java虚拟机来达到平台无关的."虚拟机"的概念是相对于"物理机"来说的,两种机器都有执行代码的能力,不过物理机是直接面向处理器.

通过字节码分析JDK8中Lambda表达式编译及执行机制

关于Lambda字节码相关的文章,很早之前就想写了,[蜂潮运动]APP 产品的后端技术,能快速迭代,除了得益于整体微服架构之外,语言层面上,也是通过Java8的lambda表达式的运用以及rxJava响应式编程框架,使代码更加简洁易维护,调用方式更加便捷.本文将介绍JVM中的方法调用相关的字节码指令,重点解析JDK7(JSR-292)之后新增的invokedynamic指令给lambda表达式的动态调用特性提供的实现机制,最后再探讨一下lambda性能方面的话题. 方法调用的字节码指令 在介绍i

JVM总结(五):JVM字节码执行引擎

JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 基于栈的字节码解释执行引擎 基于栈的指令集与基于寄存器的指令集 JVM字节码执行引擎 虚拟机是相对于“物理机”而言的,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的.因此程序员可以自行制定指令集和执行引擎的

基于栈的虚拟机字节码执行引擎

一.虚拟机字节码执行引擎概述 虚拟机字节码执行引擎主要就是研究字节码指令具体怎样被执行.对于物理机器,指令的执行是直接建立在OS和硬件的基础上 对于字节码指令的执行就是直接建立在JVM上,然后通过JVM完成具体的字节码指令到机器指令的过程.一般来说虚拟机的执行的 字节码指令是基于栈的不是采用寄存器,主要考虑的原因跨平台. 虚拟机的执行引擎是有JVM规范定义的,可以自己定义指令集以及执行引擎来执行字节码指令.不同的JVM执行引擎的实现可能不同 总体来说一个线程对应的是一个虚拟机栈:线程代码中调用的

JVM总括三-字节码、字节码指令、JIT编译执行

JVM总括三-字节码.字节码指令.JIT编译执行 java文件编译后的class文件,java跨平台的中间层,JVM通过对字节码的解释执行(执行模式,还有JIT编译执行,下面讲解),屏蔽对操作系统的依赖.一个字节(8位)可以储存256中不同的指令,这样的指令就是字节码,java所有指令有200个左右,这些指令组成了字节码文件(.class). 一.字节码的主要指令: .class文件里面的十六进制文件(如:图一),其中CAFE BABE是标志这个文件为java的编译后的文件,00000034代表

java字节码忍者禁术

Java语言本身是由Java语言规格说明(JLS)所定义的,而Java虚拟机的可执行字节码则是由一个完全独立的标准,即Java虚拟机规格说明(通常也被称为VMSpec)所定义的. JVM字节码是通过javac对Java源代码文件进行编译后生成的,生成的字节码与原本的Java语言存在着很大的不同.比方说,在Java语言中为人熟知的一些高级特性,在编译过程中会被移除,在字节码中完全不见踪影. 这方面最明显的一个例子莫过于Java中的各种循环关键字了(for.while等等),这些关键字在编译过程中会

《python源码剖析-字节码和虚拟机》

|   分类于 python源码剖析  | https://fanchao01.github.io/blog/categories/python%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/ https://fanchao01.github.io/blog/2014/12/26/python-GIL/ python源码剖析-字节码和虚拟机 发表于 2016-11-13   |   分类于 python源码剖析  | Python会将代码先编译成字节码,然后在虚拟机中动