LuaJIT 栈帧布局(stack frames layout)
创始人
2025-05-31 11:25:37
0

对于任何的目标架构(如x86、arm、loongarch),它都有一套相应的指令集。以龙芯指令集为例,指令addi.w rd rj si12,代表的意思为将物理寄存器rj中的值和立即数si12相加,将相加的结果存放在物理寄存器rd中。在CPU的内部,有一个叫做寄存器文件或者寄存器堆的硬件单元,在其里面存放着物理寄存器,每个寄存器以标识符(r0、r1...)作为地址,CPU根据地址获取/修改寄存器中的值。这些物理寄存器都是真实的存在的。

假设龙芯的处理器中没有寄存器文件这一硬件块,CPU解析执行指令addi.w rd rj si12,经过译码时就没有rj寄存器可访问、经过写回阶段也无法将数据写到rd寄存器中。这时候原本存在的loongarch指令集显然已无法使用,需要我们再去重新设计和实现一套新的指令集。除了寄存器,能够供用户可见、使用高效且管理方便的就是运行时栈了,以运行时栈作为指令的目的操作数和源操作数。如重新设计加法运算指令为addi.w frame0 frame1 si12,该指令代表的意思是,将当前函数运行时栈的1号栈帧中的值和立即数si12相加的结果存放到0号栈帧中。

回到LuaJIT,LuaJIT是一个虚拟机,用软件编写的逻辑来充当宿主机的功能,他也有供自己解释执行的指令集Bytecode。但LuaJIT本身是软件层面,没有自己的寄存器和运行时栈,只能在堆内存上面开一块空间,用来代替寄存器和运行时栈的功能。但是用堆内存代替寄存器和运行时栈在虚拟机层面的访问时效是相同(都是访问内存),所以我们可以效仿没有寄存器文件的龙芯处理器,在LuaJIT虚拟机中去除寄存器这一概念,只保留LuaJIT的运行时栈,LuaJIT也正是这么做的。LuaJIT使用的是一个叫做var stack的栈结构(定义在src/lj_frame.h中),来作为VM的运行时栈,控制着变量存放和VM中函数的调用,其Bytecode也是依据var stack来设计的。

LuaJIT前端parse source直接生成Bytecode,然后开始对Bytecode做解释执行(暂不管JIT模式),也就是执行虚拟机VM的逻辑。在进入VM之前,包括前端的parse以及后续解释计算的准备工作,都是运行C函数,程序的运行时栈符合操作系统的结构。待进入VM,此时当前函数操作系统的运行时栈(#0)已固定,不会随着VM中函数的调用(跳转)而变化,用来控制VM程序的是VM中的var stack。进入VM之前var stack的结构已经被创建好,结构如下:

--------------------------------------------------------------
| proto | nil | arg1 | ... | argn |      |     ...     | top |
--------------------------------------------------------------func    PC    base               L->top                    ^-top
  • func存放的是VM当前执行函数的闭包proto
  • PC存放上一个VM函数的PC_pre
  • base是当前函数在栈上的基地址,如slot 0代表(base+ 0*8),slot 1代表(base + 1*8)。

比如说Bytecode指令ADDVV 1 1 0,将slot 1中的值和slot 0号中的值相加,结果存放在slot 1号中。LuaJIT虚拟机实现这条指令功能对应的伪代码为:

load  t0  [base+ 0*8]
load  t1  [base+ 1*8]
add  t1  t1  t0
store  [base+ 1*8]  t1

VM中解释器执行函数调用时的整个过程,var stack结构的布局如下。callee的proto和参数传递都是在caller中完成的,其实所有编译器函数调用时都是这么做的。刚开始返回值会在callee的stack上local变量后面存放,等到PC回到caller后,会将返回值的位置往前移。

-------------------------------------------------------------------------------------------------------
| proto | nil |     |     |     | ... | proto | nil | arg0 | ... | argn | ... | ret1| ... | retn| ... |   
-------------------------------------------------------------------------------------------------------caller   PC   base    1     2   local                                   localcallee   PC   base    1     2                                 ^-top

VM中解释器执行从callee函数返回caller函数之后,var stack结构的布局如下。此时返回值已经被移到相应的位置,对照上面ret位置的变化。

------------------------------------------------------------------
| proto | nil |     |     |     | ... | ret1 | ... | retn | ... |    
------------------------------------------------------------------func    PC   base    1     2   local                           ^-top

举例来解释一下

现有如下字节码,对所有内容都已经加了注释。关于字节码的介绍可参考文章《LuaJIT Bytecode介绍》,其中有一个较为整体的介绍,对字节码不熟悉可以互相对照着看。

-- BYTECODE -- test.lua:1-11
0001    KNUM     2   0      ; 12341324  --将12341324放入slot 0
0002    KSHORT   3   0                  --将0放入slot 3
0003    ISGE     1   0                  --比较slot 1和slot 0中存放的值
0004    JMP      4 => 0007              --如果比较结果为true,跳到0007
0005    MOV      3   0                  --将slot 0中的值复制到slot 3中
0006    JMP      4 => 0008              --直接跳到0008
0007 => MOV      3   1                  --将slot 1中的值复制到slot 3中
0008 => RET1     3   2      ; result    --slot 3存放的返回值,只返回一个值-- BYTECODE -- test.lua:0-18
0001    FNEW     0   0              --根据slot 0中存放的函数原型,创建一个闭包(函数对象)并存放到slot 0中
0002    GSET     0   1      ; "max" --将slot 0中存放的闭包放到全局符号表中,key为max
0003    KSHORT   0   1              --将常量1存放到slot 0中
0004    KSHORT   1   2              --将常量2存放到slot 1中
0005    GGET     2   1      ; "max" --将全局符号表中key为max的对象放入slot 2中
0006    KSHORT   4   5              --将常量5存放到slot 4中,arg0
0007    KSHORT   5   6              --将常量6存放到slot 5中,arg1
0008    CALL     2   2   3          --slot 2为被调函数的对象,1个返回值,2个参数
0009    RET0     0   1              --slot 0存放的返回值,返回值个数为0(没有返回值)

解释器VM在解释上面字节码时,对应的var stack结构如下,仔细推敲一下。主调函数调用被调函数之前,会先把被调函数的stack结构布置好,函数对象、参数放入指定位置,并在stack上留下存放PC的空间(这个PC以后再介绍吧,其实就是放函数调用返回后,将要执行的指令的)。

-----------------------------------------------------------------------------------
| proto | nil |  1  |  2  | max |     |  5  |  6  |12341324| result |     |     | ... |     
-----------------------------------------------------------------------------------func    PC   base    1     2     3     4     5func    PC   base   1      2        3

相关内容

热门资讯

Java多线程之Executo... 文章目录1 ExecutorCompletionService1.1 简介1.2 原理1.3 Dem...
2023跨境市场洞察:金矿在哪... 就全球市场而言,跨境电商的高速增长时代已成过去时,但就意味电商金矿被挖空...
Scala中Array常用的方...         在scala中,Array有大量的方法。定义一个数组arr后ÿ...
C++基础学习笔记(四)——核... 参考链接:https://www.bilibili.com/video/BV1et41...
超详细-安装vCenterv ... 目录 介绍: 第一阶段安装: 第二阶段安装: 最近在玩虚拟...
第14届蓝桥杯STEMA测评真... [导读]:超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成,...
ChatGPT助力校招----... 1 ChatGPT每日一题:简述SPI通信协议 问题:简述SPI通信协议...
新版PMP考试难不难? 1.新版考试题量和答题时间的变化? 总题量从200道减少到180道,所以...
HBase客户端、服务器端、列... HBase客户端、服务器端、列簇设计、HDFS相关优化,HBase写性能优化切入点&#...
linux 全局环境变量删除后... linux 全局环境变量删除后 还有 仍然存在1、编辑 /etc/profile2、设置REDISC...
网站流量飙升背后:外贸企业谷歌... 自从我涉足外贸行业,我逐渐认识到谷歌SEO优化在提升网站流量和吸引潜在客户方面的重要性...
一、trino406系列 之 ... 文章目录前言Trino不是什么?Trino是什么?概览服务类型Coord...
基于Java+SpringBo...  博主介绍:专注于Java技术领域和毕业项目实战 🍅文末获取源码联系&...
财经时评|破除“内卷式”竞争 ... 作者 中国汽车工程学会理事长张进华“十四五”以来,我国智能网联新能源汽车产业坚持以科技创新引领和推动...
二十六、对象的实例化内存布局与... 一、对象的实例化 1.判断对象对用的类是否加载、链接、初始化。 2.为对象分配内存。 3.处理并发...
C语言简单工厂模式和工程创建 一,设计模式概念引入① 什么是设计模式设计模式通常被面向对象的软件开发人员所采用&#x...
新势力车企5月销量:零跑汽车再... 红星资本局6月1日消息,今日,新势力车企陆续公布5月销量数据。零跑汽车(09863.HK)再创历史新...
150.网络安全渗透测试—[C... 我认为,无论是学习安全还是从事安全的人多多少少都会有些许的情怀和使命感!...
微服务注册中心做了什么事——服... 是否被一大堆的注册中心八股文淹没,不知道哪个是哪个,有啥区别甚至于不知道...
财经时评|反内卷不能只靠自觉 ... 作者 汽车工业协会原常务副会长兼秘书长、中国动力电池产业联盟理事长董扬去年四季度以来,政府工作报告及...