当前位置: 首页 > news >正文

lua函数执行和虚拟机指令

Stack based vs Register based VM

可直接参考 Stack based vs Register based VM

lua函数调用

先看一下lua函数的结构:

/*
** Function Prototypes
*/
typedef struct Proto {CommonHeader;TValue *k; /* constants used by the function */Instruction *code;struct Proto **p; /* functions defined inside the function */int *lineinfo; /* map from opcodes to source lines */struct LocVar *locvars; /* information about local variables */TString **upvalues; /* upvalue names */TString *source;int sizeupvalues;int sizek; /* size of `k' */int sizecode;int sizelineinfo;int sizep; /* size of `p' */int sizelocvars;int linedefined;int lastlinedefined;GCObject *gclist;lu_byte nups; /* number of upvalues */lu_byte numparams;lu_byte is_vararg;lu_byte maxstacksize;
} Proto;/*
** Upvalues
*/
typedef struct UpVal {CommonHeader;TValue *v; /* points to stack or to its own value */union {TValue value; /* the value (when closed) */struct { /* double linked list (when open) */struct UpVal *prev;struct UpVal *next;} l;} u;
} UpVal;#define ClosureHeader \CommonHeader; lu_byte isC; lu_byte nupvalues; GCObject *gclist; \struct Table *envtypedef struct {ClosureHeader;Proto *p;UpVal *upvals[1];
} LClosure;

对于lua函数,luaD_precall已处理好一些前置操作,比如参数处理、增加调用栈等,然后调用luaV_execute去执行lua函数的每条指令。
特别的看一下数据栈的处理,在编译时已确定每个lua函数执行过程中数据栈的最大大小,将ci->top/L->top直接设为最大值,[L->base, L->top)就做为lua指令的“寄存器空间”使用,访问寄存器就是以下标访问base数组。

int luaD_precall (lua_State *L, StkId func, int nresults) {...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;...

指令格式

/*
** type for virtual-machine instructions
** must be an unsigned with (at least) 4 bytes
*/
typedef lu_int32 Instruction;/*
We assume that instructions are unsigned numbers.
All instructions have an opcode in the first 6 bits.
Instructions can have the following fields:`A' : 8 bits`B' : 9 bits`C' : 9 bits`Bx' : 18 bits (`B' and `C' together)`sBx' : signed BxMSB  B C A op  LSB9 9 8 6   bits
*/

根据指令的不同,参数可以表示寄存器的索引,可以表示常量的索引(Proto的TValue *k;数组),可以根据最高位是否是1决定表示寄存器还是常量的索引,还可以是上值的索引(UpVal *upvals[1];),或是其他含义。
寄存器空间不会很大,但常量数组可能会很大,而B、C的大小有限,如果B或C需要引用的常量地址超出了表示范围,在指令生成阶段,则首先会生成指令将常量装载到寄存器,然后再将B或C改为使用该寄存器地址。

常量、上值可以是lua指令的数据来源,寄存器是临时变量,都算是内部数据,那如何与“外部”进行数据交互呢?比如简单的function test() b=a end,如何读取全局变量a,又赋值给全局变量b。
是通过struct Table *env,多数情况这个env表就是_G,可参考 lua源码学习:解释器和内嵌库 load库。
将上面的test函数放到test.lua中,用luac -l -p test.lua看一下test函数的字节码:

0 params, 2 slots, 0 upvalues, 0 locals, 2 constants, 0 functions1       [1]     GETGLOBAL       0 -2    ; a2       [1]     SETGLOBAL       0 -1    ; b3       [1]     RETURN          0 1
constants (2) for 0x563c4dbf3a40:1       "b"2       "a"

对比着源代码:

  // i是当前指令,ra是A表示的寄存器的位置case OP_GETGLOBAL: {TValue g;sethvalue(L, &g, cl->env);lua_assert(ttisstring(KBx(i)));Protect(luaV_gettable(L, &g, KBx(i), ra));continue;}case OP_SETGLOBAL: {TValue g;sethvalue(L, &g, cl->env);lua_assert(ttisstring(KBx(i)));Protect(luaV_settable(L, &g, KBx(i), ra));continue;}

GETGLOBAL,从指令中取出Bx来,将其做为常量索引,取得一个TValue,这个TValue就是TString “a”,然后从_G中取得名为a的变量的值,放到寄存器上。
SETGLOBAL,ra位置存放的就是getglobal中变量a的值,将其值赋给_G中名为b的变量。

全局变量名会放到常量表中,如果是局部变量,则只是对应寄存器位置,局部变量的名字除了提供debug信息外,没有其他作用。比如function test() local c=1; b=c end的字节码:

0 params, 2 slots, 0 upvalues, 2 locals, 2 constants, 0 functions1       [1]     LOADK           0 0    ; 12       [1]     MOVE            1 0 03       [1]     SETGLOBAL       0 1    ; d4       [1]     RETURN          0 1 0
constants (2) for 0x55726251aa40:1       12       "d"
locals (2) for 0x55726251aa40:1       c       2       42       b       3       4

LOADK将常量1加载到寄存器0上,MOVE将寄存器0拷贝到寄存器1上,SETGLOBAL将寄存器1的值赋给全局变量b。

关系指令

if a==b thenprint("==")
elseprint("!=")
end

字节码:

        1       [1]     GETGLOBAL       0 -1    ; a2       [1]     GETGLOBAL       1 -2    ; b3       [1]     EQ              0 0 14       [1]     JMP             4       ; to 95       [2]     GETGLOBAL       0 -3    ; print6       [2]     LOADK           1 -4    ; "=="7       [2]     CALL            0 2 18       [2]     JMP             3       ; to 129       [4]     GETGLOBAL       0 -3    ; print10      [4]     LOADK           1 -5    ; "!="11      [4]     CALL            0 2 1

源代码:

  // i是当前指令,*pc是下条指令case OP_EQ: {TValue *rb = RKB(i);TValue *rc = RKC(i);Protect(if (luaV_equalval(L, rb, rc) == GETARG_A(i)){dojump(L, pc, GETARG_sBx(*pc)); // if条件未满足,跳过true的代码段})pc++;continue;}

A的值是0,如果rb不等于rc,pc加上第4条指令中的4,再加1后跳转到第9条指令,就是lua中if不满足的代码;如果rb等于rc,pc加1跳过第4条指令,去执行if满足的代码。
流程大概是这样的:

         CMP-------|         |----TRUE CODE     |
|                  |
|                  |
|    FALSE CODE----
|        |
|        |----OTHER CODE

另一种情况:

return a==b

字节码:

        1       [1]     GETGLOBAL       0 -1    ; a2       [1]     GETGLOBAL       1 -2    ; b3       [1]     EQ              1 0 14       [1]     JMP             1       ; to 65       [1]     LOADBOOL        0 0 16       [1]     LOADBOOL        0 1 07       [1]     RETURN          0 2

源代码:

  case OP_LOADBOOL: {setbvalue(ra, GETARG_B(i));if (GETARG_C(i)) pc++; /* skip next instruction (if C) */continue;}

A的值是1,相等的情况,跳转到第6条指令,将ra设置为1;不等的情况,跳转到第5条指令,将ra设置为0,此时c为1,会跳过第6条指令。

OP_LT(小于),OP_LE(小于等于),OP_TEST也是类似。

创建和初始化表

创建表时用NEWTABLE创建,哈希部分用SETTABLE初始化,数组部分用SETLIST初始化,比如t={1,2,3}的字节码是:

0+ params, 4 slots, 0 upvalues, 0 locals, 4 constants, 0 functions1       [1]     NEWTABLE        0 3 02       [1]     LOADK           1 1    ; 13       [1]     LOADK           2 2    ; 24       [1]     LOADK           3 3    ; 35       [1]     SETLIST         0 3 1   ; 16       [1]     SETGLOBAL       0 0    ; t
constants (4) for 0x55987156f9c0:1       "t"2       13       24       3

现将3个常数放到寄存器,再用SETLIST设置到表中。如果要创建的表包含很多数组元素,将这些元素放到寄存器时,可能需要很大的寄存器范围。SETLIST实际是分批设置的,每次设置固定数量的元素。

case OP_SETLIST: {int n = GETARG_B(i);int c = GETARG_C(i);int last;Table *h;if (n == 0) {n = cast_int(L->top - ra) - 1;L->top = L->ci->top;}if (c == 0) c = cast_int(*pc++);if (!ttistable(ra)) break;h = hvalue(ra);last = ((c-1)*LFIELDS_PER_FLUSH) + n;if (last > h->sizearray) /* needs more space? */luaH_resizearray(L, h, last); /* pre-alloc it at once */for (; n > 0; n--) {TValue *val = ra+n;setobj2t(L, luaH_setnum(L, h, last--), val);luaC_barriert(L, h, val);}continue;
}

以上代码中c就是批次,n是当前批次元素数目。
如果数据量非常大,导致批次超出了C的表示范围,那么C会被设置成0,将SETLIST指令后的指令用来存储批次。
如果使用能产生多个返回值的表达式(… 和 函数调用)初始化表数组项,如果这个表达式不是表构造的最后一项,那么只有第一个值会被使用,其他都会被丢弃;如果是最后一项,那么SETLIST中的B会被设置为0。例如:

function getlist()return 1,2,3
end
a={getlist()}
function setlist(n,...)b={...}
end
setlist(4,5,6,7)

closure

参考 upval

参考

探索Lua5.2内部实现

相关文章:

  • UWB论文:Introduction to Impulse Radio UWB Seamless Access Systems(2):脉冲;超宽带;测距;定位
  • Flutter 中的 CupertinoPicker 小部件:全面指南
  • 【MySQL精通之路】SQL优化(1)-查询优化(11)-多范围查询优化
  • 开源RAG,本地mac启动 dify源码服务
  • 2024年第十七届“认证杯”数学中国数学建模网络挑战赛D题思路(第二阶段)
  • 解锁Nginx跨域谜题:3步打造安全高效的CORS策略
  • 【Centos7+JDK1.8】Jenkins安装手册
  • MySql:多表设计-关联查询
  • slam14讲(第8讲、前端里程计)LK光流、直接法
  • 【pyspark速成专家】3_Spark之RDD编程1
  • 【数据结构】第七节:堆
  • 鸿蒙开发配置官方地图
  • Python语法学习之 - 生成器表达式(Generator Expression)
  • 【文末附gpt升级方案】UC伯克利的CV三巨头推出的纯视觉大模型在下游任务中的表现分析
  • 爬虫基本原理及requests库用法
  • (ckeditor+ckfinder用法)Jquery,js获取ckeditor值
  • [译]Python中的类属性与实例属性的区别
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • 【Amaple教程】5. 插件
  • 【EOS】Cleos基础
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • Consul Config 使用Git做版本控制的实现
  • extjs4学习之配置
  • Hexo+码云+git快速搭建免费的静态Blog
  • Python学习之路16-使用API
  • 半理解系列--Promise的进化史
  • 彻底搞懂浏览器Event-loop
  • 排序(1):冒泡排序
  • 前端面试题总结
  • 手机端车牌号码键盘的vue组件
  • 责任链模式的两种实现
  • Oracle Portal 11g Diagnostics using Remote Diagnostic Agent (RDA) [ID 1059805.
  • 直播平台建设千万不要忘记流媒体服务器的存在 ...
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ## 1.3.Git命令
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • #pragma 指令
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • #我与Java虚拟机的故事#连载18:JAVA成长之路
  • (160)时序收敛--->(10)时序收敛十
  • (2)nginx 安装、启停
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (二十四)Flask之flask-session组件
  • (南京观海微电子)——COF介绍
  • (自用)learnOpenGL学习总结-高级OpenGL-抗锯齿
  • **PHP分步表单提交思路(分页表单提交)
  • .md即markdown文件的基本常用编写语法
  • .net core 使用js,.net core 使用javascript,在.net core项目中怎么使用javascript
  • .NET MAUI学习笔记——2.构建第一个程序_初级篇
  • .net mvc部分视图
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件
  • .NET导入Excel数据
  • /dev下添加设备节点的方法步骤(通过device_create)
  • @NotNull、@NotEmpty 和 @NotBlank 区别