lua vm 三: 栈与函数调用

lua vm 运行过程中,栈是一个重要的数据结构。

栈是一个很巧妙的设计,它同时能满足 lua、c 函数运行的需要,也能实现 lua 与 c 函数的互相调用。


1. 栈


1.1 栈的数据结构

一个操作系统线程中,可以运行多个 lua vm,lua vm 用 global_State 这个结构体来表示。

一个 lua vm 中,可以运行多条 lua thread,即协程,lua thread 用 lua_State 这个结构体来表示。


图1:lua_state

每个 lua thread 都有一个专属的 “栈”,它是一个 StackValue 类型的数组,而 StackValue 内部包含了 TValue,它可以表示 lua vm 中所有类型的变量。

在 lua_State 中,用 stack 和 stack_last 这个字段来描述栈数组,stack 表示数组开头,stack_last 表示数组结尾(真实情况更复杂一点点,末尾还有 EXTRA_STACK 个元素,但问题不大)。

为了与操作系统线程的栈区别开来,这里称 lua 的这个栈为 lua 数据栈。

lua 数据栈的作用是处理函数调用以及存储函数运行时需要的数据。

栈会随着函数调用而增长,增长是通过 luaD_growstack 实现的,但有大小限制,上限为 LUAI_MAXSTACK,在 32位或以上系统中是 1000000,超过就会报 “stack overflow”。


1.2 函数调用与栈的关系

协程执行的过程,就是一个函数调用另一个函数的过程,形成一个函数调用链:f1->f2->f3->....

函数调用在 lua_State 中用 CallInfo 结构体来表示,由 CallInfo 组成的链表,即是函数调用链。

每个函数在 lua 数据栈上都占用一块空间,其范围是由 CallInfo 的两个字段表述的,func 表示起始位置,p 表示终止位置。一个函数在栈上的数据分布大概是这样的:

      0    1        n   n+1           n+m
func|arg1|arg2|...|argn|var1|var2|...|varm|

func 实际上就是 Closure 类型的数据,TValue 可以表示它,而 arg1 ~ argn 表示函数的 n 个形参,var1 ~ varm 表示函数的 m 个本地变量,形参跟本地变量在 lua 里都称为 local vars。它们是在编译期确定好各自在栈中的位置的,0 到 n+m 这些栈元素,也被称为 “寄存器”,用 R 表示,比如 R[0] 就表示 arg1,而 R[n+1] 表示 var1。

CallInfo 与 stack 的大致对应关系如下:


图2:callinfo 与 stack[1]


1.3 CallInfo 中的 top 字段

图2 中的有个细节要纠正一下,CallInfo 的 top 字段指向了栈数组中的 argn(R[n]) 项,在一些情况下,并不准确,要分情况讨论。

1、lua 函数

上图部分准确。在代码中,CallInfo 的 top 指向的是 func + 1 + maxstacksize 这个位置,maxstacksize 是在编译期确定的这个函数需要的 “寄存器” 总数量。一个普通的 lua 函数,需要的寄存器往往不止要用于存放形参,还有一些本地变量,一些运算过程的中间结果,所以 maxstacksize 往往是比形参个数大的。

比如这样一个函数:

local function f1(x, y)
	local a = x + y
end

编译出来成这样:


图3:编译结果

locals 那项显示,它至少需要 3 个寄存器,2 个用于存放形参 x 和 y,1 个用于存放本地变量 a。


2、c 函数

上图完全不准确。CallInfo 的 top 指向的应该是 func + 1 + LUA_MINSTACK 这个位置,LUA_MINSTACK 大小为 20,是初始时给 c 函数额外分配的栈空间(除了参数之外的)。

c 函数是通过 lua api 操作 lua 数据栈的,初始的时候,lua_State 的 top 和 callinfo 的 top 都是指向 argn 的位置的。随着 c 函数的运行,比如通过 lua_push 开头的 api 往栈里面压 n 个数据,top 就相应的增长 n 个位置。

这也是 lua 数据栈的巧妙之处:

  • 当一个 lua 函数调用一个 c 函数,就先把参数放到栈上,而 c 函数被 op_call 的时候,它又可以通过 lua_to 开头的 api 把栈上保存的参数转换成 c 函数自己的变量。

  • 当一个 c 函数调用一个 lua 函数时,先通过 lua_push 开头的 api 往栈里压 n 个参数以及 lua 函数,然后再调用 lua_call 完成调用,而调用完成后,lua 函数的返回结果又都保存在栈上,这时候 c 函数又可以通过 lua_to 开头的命令获取这些返回结果。


值得注意的是,写 c 函数的时候,要时刻注意栈空间的大小是否足够。这种情况下 lua 不会惯着你了,初始时只提供了额外的 LUA_MINSTACK 个元素的栈空间。当栈空间不够的时候,要使用 luaL_checkstack 来扩容。


1.4 固定参数的函数调用

固定参数的函数调用比较简单,比如这样一个简单的打印函数:

local function f1(x, y, z)
    print(x, y, z)
end

f1(10, 20, 30)

编译出来是这样的:


图4:固定参数函数调用的编译结果

f1 调用 print 的过程中,栈空间布局是这样的:


图5:固定参数函数调用的栈空间布局

整个调用过程可以归结为三步:

第一、把 print 这个函数入栈。

1	GETTABUP	3 0 0	; _ENV "print"

通过 GETTABUP 指令,从 _ENV 中把 print 这个函数 (实际上是closure) 复制到 R[3] 寄存器上。


第二、把三个参数入栈。

2	MOVE	4 0	
3	MOVE	5 1	
4	MOVE	6 2

x,y,z 分别是在 R[0],R[1],R[2],通过 MOVE 指令,把它们复制到 R[4],R[5],R[6] 这几个寄存器上。


第三、调用 print 函数。

5	CALL	3 4 1	; 3 in 0 out

OP_CALL 的格式是 OP_CALL,/* A B C R[A], ... ,R[A+C-2] := R[A](R[A+1], ... ,R[A+B-1]) */

A 表示函数的位置,即 R[3];
B 表示参数的个数,此处参数个数是确定的,B-1 等于参数个数,这里 B 是 4,刚好对应 3 个参数;
C 表示返回值的个数,1 表示没有返回值;



以上也可以看出,函数占用的栈空间是堆叠在一起的。f1 函数调用 print 函数的时候,要在自己的栈空间上先放入 print 函数的 closure,再放入参数。而当 print 函数开始执行,从 print 函数开始的这段空间又被 print 函数当成自己的栈空间。


1.5 不定参数的函数调用

不定参数调用的时候,比较复杂。当参数个数确定的时候,可以让 OP_CALL 的参数 B 来表示个数,当参数不确定的时候,只能用别的办法。

举个例子:

local function f1()
    local t = {10, 20, 30}
    print(table.unpack(t))
end

编译出来是这样:


图6:不定参数函数调用的编译结果

大致步骤如下:

第一、构造表 t,构造完后 t 放在 R[0] 处。

1	NEWTABLE	0 0 3	; 3
2	EXTRAARG	0	
3	LOADI	1 10	
4	LOADI	2 20	
5	LOADI	3 30	
6	SETLIST	0 3 0

第二、从 _ENV 取 print 函数放在 R[1] 处。

7	GETTABUP	1 0 0	; _ENV "print"

第三、从 _ENV 取 table.unpcak 函数放在 R[2] 处。

8	GETTABUP	2 0 1	; _ENV "table"
9	GETFIELD	2 2 2	; "unpack"

第四、从 R[0] 复制表 t 到 R[3] 处。

10	MOVE	3 0

第五、调用 R[2] 处的 table.unpack 函数,所有的结果放在从 R[2] 开始的寄存器处。B = 2 表示只有一个参数 t,C = 0 表示返回所有的结果,并且个数不确定,这时候 lua vm 会把 L->top 设置为 “最后一个结果”,这样后续的函数调用就可以把它当成不确定个数的参数。

11	CALL	2 2 0	; 1 in all out

第六、调用 R[1] 处的 print 函数,A = 1 表示函数在 R[1] 处;B = 0 表示参数个数不确定,这时候参数就是从 R[A+1] 开始,到 R[A+L->top]为止,即 R[2] 到 R[1+L->top],刚就就是第五步 table.unpack 返回的所有结果。

12	CALL	1 0 1	; all in 0 out

总结一下,在不定参数的调用中,产生不定参数的函数把结果放到栈上,并设置 L->top 指向最后一个结果,而调用方在 OP_CALL 时把 B 设为 0,表示使用栈上 R[A+1] 到 R[A+L->top] 这一段栈空间上的所有元素作为参数。


1.6 OP_CALL 的完整规则[2]

Syntax

CALL A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))

Description

Performs a function call, with register R(A) holding the reference to the function object to be called. Parameters to the function are placed in the registers following R(A). If B is 1, the function has no parameters. If B is 2 or more, there are (B-1) parameters. If B >= 2, then upon entry to the called function, R(A+1) will become the base.

If B is 0, then B = ‘top’, i.e., the function parameters range from R(A+1) to the top of the stack. This form is used when the number of parameters to pass is set by the previous VM instruction, which has to be one of OP_CALL or OP_VARARG.

If C is 1, no return results are saved. If C is 2 or more, (C-1) return values are saved. If C == 0, then ‘top’ is set to last_result+1, so that the next open instruction (OP_CALL, OP_RETURN, OP_SETLIST) can use ‘top’.


2. 参考

[1] codedump. Lua 设计与实现. 北京: 人民邮电出版社, 2017.8: 45.

[2] dibyendumajumdar. op-call-instruction. Available at https://the-ravi-programming-language.readthedocs.io/en/latest/lua_bytecode_reference.html#op-call-instruction.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/679024.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

异常概述

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 在程序运行过程中,经常会遇到各种各样的错误,这些错误统称为“异常”。这些异常有的是由于开发者将关键字敲错导致的&#xf…

《QT从基础到进阶·四十二》QT运行后项目图标,exe图标问题,VS加载.pro文件问题

1、QT图标有时候不能正常显示,不管是加到qrc还是用绝对路径,都无法正常显示,之前是可以的,具体原因目前还不太清楚,我在VS项目——vcpkg——use vcpkg把否改为是就可以了 2、出现无法定位程序输入点的报错&#xff0c…

408数据结构-图的存储与基本操作 自学知识点整理

前置知识:图的基本概念 图的存储必须完整、准确地反映顶点集和边集的信息。根据不同图的结构和算法,采用不同的存储方式将对程序的效率产生相当大的影响,因此选取的存储结构应适合于待求解的问题。 图的存储 邻接矩阵法 所谓邻接矩阵存储&a…

CSS(盒子模型,定位,浮动,扩展)

CSS 盒子模型:外边距:内边距:水平居中: 定位:相对定位:绝对定位:固定定位: 浮动:扩展: 盒子模型: 盒子模型(Box Model) 规定了元素框处理元素内容…

免费,Scratch蓝桥杯比赛历年真题--第15届蓝桥杯STEMA真题-2024年3月份(含答案解析和代码)

第15届蓝桥杯STEMA真题-2024年3月份 一、单选题 答案&#xff1a;D 解析&#xff1a;y坐标正值表示上&#xff0c;负值表示下&#xff0c;故答案为D。 答案&#xff1a;C 解析&#xff1a;18<25为真&#xff0c;或关系表示一真即为真&#xff0c;故答案为C。 答案&#xff…

【Linux】常用基本指令汇总

前言&#xff1a; 本章将介绍Linux操作系统常用的基本指令&#xff0c;另外&#xff0c;使用这些指令编辑一个shell脚本&#xff0c;方便大家理解使用。 目录 常用指令whoamipwdls关于iNode的解释验证标识文件的方式 cdtouchmkdir&#xff08;重要&#xff09;treemdir指令 &a…

radsystems教程的基本使用之时间字段范围检索

前言&#xff1a; 根据之前的文章&#xff0c;我相信大部分人都能够做到&#xff0c;页面的数据展示&#xff0c;基本的查询功能。我们知道的是这个数值范围检索是非常容易实现的&#xff0c;但是这个时间字段范围检索并不是很如愿。 细心的朋友会发现每次用Date Fied这个组件…

6月4号总结

目录 刷题记录(Codeforces Round 916 (Div. 3)A~D) 1.A. Problemsolving Log 2.B. Preparing for the Contest 3.C. Quests 4.D. Three Activities 刷题记录(Codeforces Round 916 (Div. 3)A~D) 1.A. Problemsolving Log Problem - A - Codeforces A问题要学1分钟&#x…

2-异常-FileNotFoundException(三种不同的报错)

2-异常-FileNotFoundException(三种不同的报错) 更多内容欢迎关注我&#xff08;持续更新中&#xff0c;欢迎Star✨&#xff09; Github&#xff1a;CodeZeng1998/Java-Developer-Work-Note 技术公众号&#xff1a;CodeZeng1998&#xff08;纯纯技术文&#xff09; 生活公众…

链表的讲解与单链表的实现

链表的讲解与单链表的实现 一、链表的概念及结构二、链表的分类三、单链表的实现(使用VS2022)1.销毁、打印内容2.尾插尾删、头插头删尾插尾删头插头删 3.查找、指定插入、指定删除查找指定插入指定删除 4.目标后一个插入、目标后一个删除 四、完整 SList.c 源代码 一、链表的概…

一个简约而不简单的记账 App(一刻记账)

前言 在两年多前, 我曾经写过一个本地化的记账 App, 当时没有想过以后的发展. 全程是本地化的, 当时主要是为了练习 Compose 而写的. TallyApp 而现在一个完整的记账 App 它来了 一刻记账 算是圆了我两年前的梦了吧. 也让我可以真正的使用自己的记账软件. 下面是 一刻记账 记…

联发科COMPUTEX展台有看点,2024年最热AI技术都在这里

近日&#xff0c;备受瞩目的COMPUTEX 2024科技展会开幕&#xff0c;联发科在COMPUTEX展出了先进AI技术成果和AI在广泛领域的创新应用。从不久前召开的天玑开发者大会MDDC来看&#xff0c;联发科的AI关键技术和AI应用生态早已遍地开花&#xff0c;全面覆盖智能手机、平板电脑、汽…

【图 - 遍历(BFS DFS)】深度优先搜索算法(Depth First Search), 广度优先搜索算法(Breadth First Search)

图的深度优先搜索(Depth First Search)&#xff0c;和树的先序遍历比较类似; 广度优先搜索算法(Breadth First Search)&#xff0c;又称为"宽度优先搜索"或"横向优先搜索"。 深度优先搜索 深度优先搜索介绍深度优先搜索图解有向图的深度优先搜索广度优先搜…

vue2中封装图片上传获取方法类(针对后端返回的数据不是图片链接,只是图片编号)

在Vue 2中实现商品列表中带有图片编号,并将返回的图片插入到商品列表中,可以通过以下步骤完成: 在Vue组件的data函数中定义商品列表和图片URL数组。 创建一个方法来获取每个商品的图片URL。 使用v-for指令在模板中遍历商品列表,并显示商品名称和图片。 下面是一个简单的Vu…

【Vue】成绩案例

文章目录 一、功能描述二、思路分析三、完整代码 一、功能描述 1.渲染功能 2.删除功能 3.添加功能 4.统计总分&#xff0c;求平均分 二、思路分析 渲染功能 v-for :key v-bind:动态绑定class的样式&#xff08;来回切换&#xff09; 删除功能 v-on绑定事件&#xff0c; 阻止…

JVM学习-MAT

MAT(Memory Analyzer Tool) 基本概述 Java堆内存分析器&#xff0c;可以用于查找内存泄漏以及查看内存消耗情况MAT是基于Eclipse开发的&#xff0c;不仅可以单独使用&#xff0c;还能以插件方式嵌入Eclipse中使用&#xff0c;是一款免费的性能分析工具 获取堆dump文件 dump…

目标检测数据集 - 智能零售柜商品检测数据集下载「包含VOC、COCO、YOLO三种格式」

数据集介绍&#xff1a;智能零售柜商品检测数据集&#xff0c;真实智能零售柜监控场景采集高质量商品图片数据&#xff0c;数据集含常见智能零售柜商品图片&#xff0c;包括罐装饮料类、袋装零食类等等。数据标注标签包含 113 个商品类别&#xff1b;适用实际项目应用&#xff…

K8s Pod的QoS类

文章目录 OverviewPod的QoS分类Guaranteed1.如何将 Pod 设置为保证Guaranteed2. Kubernetes 调度器如何管理Guaranteed类的Pod Burstable1. 如何将 Pod 设置为Burstable2.b. Kubernetes 调度程序如何管理 Burstable Pod BestEffort1. 如何将 Pod 设置为 BestEffort2. Kubernete…

【CUDA】保姆级用学校服务器远程编写运行CUDA代码-jupyter

用学校服务器远程编写运行CUDA代码 0 前言1 检查当前系统是否支持CUDA2 在 Jupyter 中编写和执行代码3 打开终端 激活pytorch环境4 创建新文件&#xff08;.cu格式&#xff09;5 执行代码 0 前言 关于如何用xshell连学校服务器&#xff0c;我在之前的博客中已经详细介绍了&…

从零开始利用MATLAB进行FPGA设计(七)用ADC采集信号教程2

黑金的教程做的实在太拉闸了&#xff0c;于是自己摸索信号采集模块的使用方法。 ADC模块&#xff1a;AN9238 FPGA开发板&#xff1a;AX7020&#xff1b;Xilinx 公司的 Zynq7000 系列的芯片XC7Z020-2CLG400I&#xff0c;400引脚 FBGA 封装。 往期回顾&#xff1a; 从零开始利…