1.解Lua分代GC
2.Lua 5.4之指令编码格式
3.Lua5.4 源码剖析——虚拟机6 之 OpCode大全
4.《Lua5.4 源码剖析——基本数据类型 之 布尔类型》
5.第二十四期C++/lua手游多开脚本全套课程
6.Lua字节码文件结构及加载过程
解Lua分代GC
一直对GC很感兴趣,码解最近阅读Lua GC相关资料并结合Lua5.4.6源码总结了Lua的码解分代GC机制。
在Lua中,码解对象根据年龄被划分为新旧不同阶段。码解其中,码解NEW、码解随机生成源码SURVIVAL属于新对象;OLD、码解OLD0、码解OLD1、码解TOUCHED1、码解TOUCHED2属于老对象。码解
对象的码解颜色表示其状态,分为黑、码解白、码解灰三种。码解黑色代表对象已被完全标记,灰色代表有待标记,白色代表不再被使用的对象。YoungGC通过递归标记灰色对象,清理白色对象。
对象颜色通过mark字段中的三个比特位表示,黑色占一个比特,白色占两个比特,全0表示灰色状态。
使用三色标记方法,对象颜色动态变化,帮助GC准确识别无用对象。
在Lua GC中,对象的管理通过特定的链表结构实现,包含普通无析构函数对象链表、有析构函数对象链表以及灰色对象链表。
新对象经历两次小GC才能成为老对象的机制,旨在确保新对象的生命周期大于一次年轻代GC间隔,避免错误标记。
源码中只标记OLD1年龄态对象的原因是,G_TOUCHED1、G_TOUCHED2、OLD0年龄态对象已经在灰链中。而OLD年龄态在小GC时不进行引用标记。
OLD1年龄态对象已经历两次小GC,理论上属于老对象范畴。但将其直接归并入OLD态会导致SURVIVAL年龄态对象的引用标记问题。
通过上述机制,Lua的分代GC实现了高效而精准的对象管理,降低了内存碎片,提升了程序性能。
Lua 5.4之指令编码格式
踏入Lua 5.4的虚拟机世界,指令编码格式的革新无疑为性能和灵活性带来了全新风貌。从Lua 5.3的4字节定长指令,到5.4的革新设计,我们看到了一个更为精炼且高效的操作码体系。 指令编码的转型 在Lua 5.4,操作码不再是单一的4位,而是扩展到了7位,为更多的指令提供了可能性。同时,参数编码方式也发生了显著变化,7位操作码后,剩下的位被用于精细地编码多达3个参数,其中包括一个新增的标志位,这使得参数处理更为灵活。值得一提的是,之前的iABC模式简化了,如ADDK指令直接处理常量,通达信精准做短指标源码R[A]的新值由R[B]和R[C]相加得出,而RK指令的使用则由标志位k来决定索引类型。 模式的增新与优化iAx,从5.3到5.4的融合: 在Lua 5.4中,iAx模式的Ax参数扩展了2位,用于EXTRAARG伪指令,如LOADKX的使用,展现出了对复杂操作的处理能力。
isJ,新出现的符号整数指令: isJ模式专为有符号整数设计,仅在JMP指令中应用,体现了Lua 5.4对不同数据类型的精细化支持。
编码细节的演变- 指令结构变得更紧凑,每个操作码的精确度提升,带动了指令集的扩展。
- 参数的位数调整,使得指令的紧凑性与表达力达到平衡。
- 新增的iABC编码标志位k,赋予了指令更多的灵活性和适应性。
- 部分指令的长度从8字节压缩,提升了虚拟机的运行效率。
编码实例解析- Excess-K编码示例: 这一变化在处理有符号整数时尤为明显,4比特整数的编码演示了新编码方式的高效和紧凑。
迈向未来Lua 5.4的指令编码革新,不仅提升了虚拟机的性能,还展示了Lua语言在发展中的革新精神。深入理解这些变化,无疑能帮助开发者更好地驾驭这一强大的脚本语言。欲了解更多细节,敬请关注后续深度解析文章。
Lua5.4 源码剖析——虚拟机6 之 OpCode大全
深入探索Lua5.4虚拟机的奥秘——OpCode大揭秘 在Lua5.4的世界里,多个精心设计的OpCode构成了其强大的指令集,它们像乐谱上的音符,驱动着程序的旋律。让我们一起走入Lua5.4的虚拟机,逐个解析这些关键的指令代码单元。数据加载乐章
首先,我们来到数据加载的舞台,OpCode在这里翩翩起舞:OP_MOVE: 轻盈地将值从一个寄存器转移到另一个,就像调色板上的颜色流转。
OP_LOADI/OP_LOADF/OP_LOADK/OP_LOADKX: 数字的音符——整数、浮点数、常量和UpValue,一一奏响。
OP_LOADTRUE/OP_LOADFALSE: 布尔值的二元抉择,为逻辑运算注入力量。
OP_LOADNIL/OP_GETUPVALUE/OP_GETTABUP: 无尽的赋值之路,从零开始,直至无穷。
算术运算交响曲
接着,我们进入算术运算的篇章,OpCode在此处激荡:从简单的OP_ADDK(R[A]:=R[B]+K[C])到OP_SUBK、OP_MULK、OP_MODK,再到OP_POWK和OP_DIVK,每个都是音符间的和谐对话。
直接数字运算,如OP_ADDI(R[A] = R[B] + sC),界限清晰,无需预存,如音乐中的即兴演奏。
寄存器间的算术运算,如OP_ADD、怎么看源码有没有bs架构OP_SUB等,像弦乐四重奏中的协奏。
位运算与Table操作
然后,我们步入位运算和Table操作的篇章,它们是程序逻辑的精密齿轮:OP_BANDK、OP_BORK和OP_BXORK,与数字或寄存器进行二进制对话,像编钟的和谐共鸣。
OP_SHL和OP_SHR,位移的旋律,为数据结构增添深度。
OP_NEWTABLE创生新表,OP_GETI/GETFIELD/GETTABLE查询信息,OP_SETI/SETFIELD/SETTABLE则进行修改,像编排一场数据舞蹈。
元方法与函数调用
接下来,元方法与函数调用的乐章,OpCode在其中担任指挥:MMBIN、MMBINI和MMBINK,元方法调用的三种旋律,为对象赋予魔法。
OP_CALL和OP_TAILCALL,函数调用的起始与结束,像指挥家的挥棒和收棒。
OP_VARARGPREP和OP_VARARG,处理可变参数,为函数调用增添变奏。
跳转与控制流
最后,我们来到指令的跳跃和控制流部分,OP_JMP如同指挥棒,引导程序的旋律:OP_JMP的精确跳跃,如同乐章的节奏变化,控制程序的进程。
在Lua 5.4中,goto的加入,让程序的流程更加灵活。
等式判断与循环
等式判断与循环的OpCode,如同交响乐的高潮,丰富而有力:OP_EQ、OP_LT、OP_LE、OP_GTI、OP_GEI,比较与判断,赋予逻辑深度。
OP_TEST和OP_TESTSET,条件判断与赋值的巧妙结合。
OP_FORPREP和OP_TFORPREP,循环的启动与准备,OP_FORLOOP和OP_TFORCALL,执行旋律的反复。
杂项OpCode的精彩点缀
最后,8个杂项OpCode为乐章画上完满的句号:OP_UNM:数值取负,反转音符的旋律。
OP_BNOT:位取反,逻辑的翻转。
OP_NOT:条件取反,为逻辑增添复杂性。
OP_LEN:求对象长度,探索数据的深度。
OP_CONCAT:字符串拼接,连接旋律的片段。
OP_SETLIST:创建列表,新版乌龙大掌柜十三水源码初始化的序曲。
深入理解Lua5.4的OpCode,就像欣赏一场丰富的音乐盛宴,每一个音符都蕴含着程序的智慧与力量。让我们沉浸在这奇妙的虚拟机世界,继续探索更深层次的编程奥秘。祝你乐在其中,收获满满!《Lua5.4 源码剖析——基本数据类型 之 布尔类型》
《Lua5.4 源码剖析——基本数据类型 之 布尔类型》
Lua的基本数据类型中,布尔类型是最简单的一种。在Lua中,尽管通常认为布尔类型只有true和false两种值,但实际上,其在源码中的实现更为精细。Lua使用了TValue这个数据结构来存储所有类型,包括布尔类型。TValue包含了一个lu_byte类型的tt_(类型标记)和Value类型的value_(存储实际数据)。
tt_字段占用1个字节,其中4个位用于存储基本类型(0-8代表nil到thread),2个位用于表示类型变体,1个位用于垃圾回收标志。布尔类型通过类型变体实现,它被声明为LUA_TBOOLEAN,当tt_的第5位为0时代表false,为1时代表true。
判断布尔变量的宏定义在《lobject.h》中,而布尔类型的实际值并不存储在value_,而是直接在tt_字段中,以节省内存和判断复杂度。理解了这一点,我们就可以深入理解Lua中布尔类型的内存结构和使用方式。继续关注后续章节,将探讨其他基本数据类型在Lua5.4源码中的实现细节。
第二十四期C++/lua手游多开脚本全套课程
第一章 C++ 多线程脚本框架篇(年最新全套框架) 深入探索C++开发环境VS,构建一站式多开脚本解决方案。本章涵盖以下核心功能:注册定制版大漠,优化性能与体验。
集成注册普通版及内部版大漠,实现多版本兼容。
调用大漠的各类函数,包括鼠标、键盘、窗口、汇编等,实现高效控制。
开发多线程手游脚本基本界面,提升用户体验。
引入多页面切换的TAB控件,增强界面交互性。
添加删除任务、导入账号、右键菜单等实用功能,优化管理流程。
集成控制台一键启动与停止,实现高效脚本控制。
调整多开数量与延时启动,个性化设置脚本行为。
循环启动模拟器并逐个绑定,实现智能化脚本执行。
控制台一键暂停与恢复功能,确保脚本稳定运行。
注册热键,简化操作流程,提高工作效率。app源码开放了有什么用
实现保存与读取配置,提升脚本使用的便利性。
分析控制台单停单开功能,优化脚本执行策略。
引入ADB命令调用功能,支持多平台脚本操作。
构建多角色切换逻辑,适应复杂游戏环境。
实现掉线重连与掉窗口重开功能,增强脚本鲁棒性。
增加网络验证系统,保障脚本安全运行。
对验证系统增加防破解暗桩,提高系统防护能力。
集成远程答题系统,提升游戏策略与效率。
完善全套脚本框架,满足多元游戏需求。
第二章 游戏脚本实战 将理论知识付诸实践,深入剖析游戏脚本开发。本章聚焦以下实战应用:多开自动登录功能的开发,简化游戏入门流程。
多开自动登录功能的深入优化,提高自动化程度。
主线任务的分阶段开发,实现任务自动化执行。
师门任务的自动化处理,提升玩家游戏效率。
宝图任务的自动化获取,加速游戏资源积累。
福利领取自动化,确保玩家权益最大化。
角色加点与技能升级的自动化管理,优化角色成长路线。
装备锻造与镶嵌自动化,提高装备品质。
背包清理自动化,保持游戏资源有序。
清水湾降妖任务自动化,提升游戏体验。
本机组队的自动化操作,优化团队协作。
第三章 C++ 调用lua5.4.4 探索C++与lua5.4.4的整合之道,为游戏脚本注入更多灵活性与可扩展性。本章涵盖:在控制台下构建lua环境,实现脚本语言的集成。
注册C/C++函数至控制台lua环境,实现功能扩展。
执行控制台lua脚本文件,灵活调用游戏逻辑。
构建MFC环境下lua环境,适应复杂界面需求。
实现MFC与lua函数的交互访问,提高脚本的适应性。
获取注册至lua的C++函数返回值,优化脚本调用流程。
构建主线任务脚本,提升自动化处理能力。
通过实例演示lua_getglobal、lua_pushnumber和lua_pcall的使用方法。
通过实例演示lua_type、lua_gettop和lua_isnumber等函数,掌握lua栈的遍历与管理。
Lua字节码文件结构及加载过程
在探索Lua的世界中,字节码文件结构与加载过程是程序运行效率的关键。本文将为你揭示Lua 5.4.3的内部奥秘,从文件头到函数块,逐一剖析其构造与加载流程。让我们一起深入理解Lua的加载逻辑,通过luaU_undump函数,将源代码编织成二进制的指令织锦。
首先,Lua字节码文件由文件头和函数块两大部分组成,如同编织的经纬线,共同构建起程序的基石。文件头包含了Lua的签名("\x1bLua")、版本号(例如Lua 5.4.3的)、官方格式号(0)以及LUAC_DATA的校验信息。紧接着是指令、整型和浮点型大小的指示,每个部分都承载着特定的含义,确保文件的正确性。
深入解析luaU_undump函数,它是Lua加载阶段的灵魂,处理着二进制文件的字节码。这个函数首先会进行头检查,包括Lua签名、版本号、格式号等,随后是lua_Integer和lua_Number的长度指示。例如,Lua 5.4.3的头字节包含整型、浮点型校验和文件头信息,通过反编译,我们可以看到函数原型的细节,如函数名、参数、行数和指令数量等。
在Lua 5.4.3的loadFunction函数中,我们看到函数块被精细划分,包括源代码、行号、参数、可变参数、栈大小、字节码、常量、上值和闭包等元素。这些元素通过loadStringN、loadUnsigned等函数逐一加载,确保每个部分都按照特定规则组织。
文件结构的关键部分包括loadConstants(如main函数中的常量"a")、loadUpvalues(如全局表"_ENV"的加载)、loadProtos(函数内部原型的加载),以及loadDebug信息,如行号和变量名称。比如,main函数的4个指令和a函数的5个,以及upvalues数据" 5f 4e",都在这个过程中得到解析。
在Lua的实现中,lauxlib.c、lapi.c、ldo.c和lundump.c等核心文件,以及lua_load、f_parser、luaU_undump、checkHeader和loadFunction等核心函数,共同构建起字节码的编译和加载流程。从源代码到二进制,再到虚拟机的执行,每一个环节都经过精心设计,以达到高效的运行效率。
总的来说,Lua字节码文件结构的复杂性映射出其内在的执行效率。理解这些细节,不仅有助于我们编写更优化的脚本,也让我们对Lua的底层机制有了更深的认识。让我们继续探索luaU_undump的更多奥秘,揭开Lua字节码加载过程的神秘面纱。
《Lua5.4 源码剖析——基本数据类型 之 数字类型》
数字类型在编程中分为整数和浮点数两种。在Lua语言的5.3版本之前,所有数字都被底层实现为浮点数,整数的概念并未独立出来,而是通过浮点数的IEEE表示法进行表示与数据存储。这样,在进行整数运算时,可能会在多次运算后累积产生出意外的浮点误差。因此,从Lua5.3版本开始,Lua引入了对整数的支持,使其不再依赖于浮点数进行表示,并且支持位运算等整数运算操作符。
在Lua语言中,每个基础对象需要存储其类型标识,这个标识在源码《lua.h》中定义为tt,数字类型的tt枚举值为LUA_TNUMBER(对应数字3)。由于数字类型分为整型和浮点型,它们通过类型变体来区分。在源码《lobject.h》中,类型变体LUA_VNUMINT表示整型,而LUA_VNUMFLT表示浮点型。
数字类型在TValue中定义了Value字段,这个字段包含i和n两个字段,用于分别存储整型和浮点型的数值。在历史原因的影响下,lua_Number并不是指所有数字类型,而是专门指浮点类型;lua_Integer则专门指整型。因此,设置整数或浮点数时,需要先设置Value字段中的n字段(整型)或i字段(浮点型),然后使用settt_宏设置type tag(tt)字段为对应值LUA_VNUMFLT或LUA_VNUMINT。
在底层,数字类型的数据类型具体表现为lua_Integer和lua_Number。在源码《lua.h》中声明,lua_Number为LUA_NUMBER,lua_Integer为LUA_INTEGER。深入学习它们的定义,可以看到整型有int、long、long long三种类型,浮点型有float、double、long double三种类型。Lua5.4的默认配置中,整型使用long long类型,浮点型使用double类型。在Windows平台上,整型使用__int类型。
至此,数字类型的讲解就告一段落。希望本文对理解Lua语言中的数字类型有所帮助。
《Lua5.4 源码剖析——基本数据类型 之 Function》
在编程语言中,函数作为重要的元素,可以分为第一类值语言和第二类值语言。第一类值语言如Lua,其函数与数值类型、布尔类型地位相同,可动态创建、存储与销毁;第二类值语言则无法实现这些操作。Lua是第一类值语言,支持动态函数创建与销毁。
在Lua中,函数的基本类型枚举为LUA_TFUNCTION,对应8位二进制为 。函数类型变体包括三种:LUA_VLCL(Lua闭包)、LUA_VLCF(C函数指针)和LUA_CCCL(C语言闭包)。闭包由函数与UpValue组成,UpValue为在当前函数外声明但函数内可以访问的变量,类似于局部变量但具备一定作用域。
闭包分为C类型闭包与Lua类型闭包。C类型闭包在Lua源代码中由C语言实现,主要用于调用C函数。Lua类型闭包则在Lua中动态创建,支持多层嵌套与UpValue管理。闭包实现方式包括C语言闭包和Lua闭包。
Lua闭包由ClosureHeader宏定义,包含闭包的类型标识、UpValue数组长度、垃圾回收列表等信息。闭包内部的函数通过Proto数据结构定义,包含参数数量、最大寄存器数量、UpValue数量等属性。Lua闭包中的UpValue通过UpVal类型管理,UpVal状态分为open和close两种,open状态时UpVal存储在链表中,close状态时UpVal的值被保存,直到函数返回时才被销毁。
在实现多返回值时,Lua通过调整运行堆栈的结构,将多个返回值合并,减少内存使用。在尾调用消除中,Lua在函数执行结束时,复用当前函数的栈空间进行下一次函数调用,避免了堆栈溢出的问题。Lua的尾调用优化使得函数调用效率更高,程序运行更稳定。
Lua5.4 源码剖析——虚拟机2 之 闭包与UpValue
故事将由我们拥有了一段 Lua 代码开始,我们先用 Lua 语言写一段简单的打印一加一计算结果的 Lua 代码,并把代码保存在 luatest.lua 文件中:
可执行的一个 Lua 文件或者一份单独的文本形式 Lua 代码,在 Lua 源码中叫做 "Chunk"。无论我们通过什么形式去执行,或者用什么编辑器去执行,最终为了先载入这段 Lua 的 Chunk 到内存中,无外乎会归结到以下两种方式:1)Lua 文件的载入:require 函数 或 loadfile 函数;2)Lua 文本代码块的载入:load 函数;这两种方式最终都会来到下面源码《lparse.c》luaY_parser 函数。该函数是解析器的入口函数,负责完成代码解析工作,最终会创建并返回一个 Lua 闭包(LClosure),见下图的红框部分:
另外,上图中间有一行代码最终会调用到 statement 函数,statement 函数是 Chunk 解析的核心函数,它会一个一个字符地处理我们编写的 Lua 代码,完成词法分析和语法分析工作,想要了解字符处理整个状态流程的可以自行研读该部分源码,见源码《lparse.c》statement 函数部分代码:
完成了解析工作之后,luaY_parser 函数会把解析的所有成果放到 Lua 闭包(LClosure)对象之中,这些存储的内容能保证后续执行器能正常执行 Lua 闭包对应的代码。
Lua 闭包由 Proto(也叫函数原型)与 UpValue(也叫上值)构成,见源码《lobject.h》LClosure 定义,我们下面将进行详细的讲解:
UpValue 是 Lua 闭包数据相关的,在 Lua 的函数调用中,根据数据的作用范围可以把数据分为两种类型:1)内部数据:函数内部自己定义的数据,或者通过函数参数的形式传入的数据(在 Lua 中通过参数传入的数据本质上也是先赋值给一个局部变量);2)外部数据:在函数的更外层进行定义,脱离了该函数后仍然有效的数据;外部数据在我们的 Lua 闭包中就是 UpValue,也叫上值。
既然 Lua 支持函数嵌套,也知道了 UpValue 本质就是上层函数的内部数据。那么 UpValue 有必要存储于 Lua 闭包(LClosure)结构体当中吗?是为了性能考虑而做的一层指针引用缓存吗?回答:并不是基于性能的考虑,因为在实际的 Lua 运用场景中,函数嵌套的层数通常来说不会太多,个别函数多一层的查询访问判断不会带来过多的性能开销。需要在闭包当中存储 UpValue 主要原因是因为内存。Lua 作为一门精致小巧的脚本语言,设计初衷不希望占用过多的系统内存,它会尽量及时地清理内存中用不到的对象。在嵌套函数中,内层函数如果仍然有被引用处于有效状态,而外层函数已经没有被引用了已经无效了,此时 Lua 支持在保留内层函数的情况下,对外层函数进行清除,从而可以清理掉外层函数引用的非当前函数 UpValue 用途以外的大量数据内存。
尽管外层函数被清除了,Lua 仍然可以保持内层函数用到的 UpValue 值的有效性。UpValue 如何能继续保持有效,我们在之前的基础教程《基本数据类型 之 Function》里面学习过,主要是因为 UpValue 有 open 与 close 两种状态,当外层函数被清除的时候,UpValue 会有一个由 open 状态切换到 close 状态的过程,会对数据进行一定的处理,感兴趣的同学可以回到前面复习一下。
UpValue 有效性例子
接下来我们举一个代码例子与一个图例,表现一下 UpValue 在退出外层函数后仍然生效的情况,看一下可以做什么样的功能需求,加深一下印象,请看代码与注释:
上述代码在执行 OutFunc 函数后,外层的 globalFunc 函数变量完成了赋值,每次对它进行调用,都将可以对它引用的 UpValue 值即 outUpValue 变量进行正常加 1。
函数的内部数据属于函数自身的内容,外部其它函数无法通过直接的方式访问其它函数的内部数据。函数自身的东西会存在于 LClosure 结构体的 Proto*p 字段中。Proto 全称 "Function Prototypes",通常也可以叫做 "函数原型",我们来看一下它的定义,见源码《lobject.h》Proto 结构体:
结构体字段比较多,我们先不细看,后面用到哪个字段会再进行补充说明。函数的内部数据分为常量与变量(即函数局部变量),分别对应上图的如下字段:
1)常量:TValue* k 为指针指向常量数组;int sizek 为函数内部定义的常量个数,也即常量数组 k 的元素个数。
2)局部变量:LocVar* locvars 为指针指向局部变量数组;int sizelocvars 为函数定义的局部变量个数,也即局部变量数组 locvars 的元素个数。
UpValue 的描述信息会存储在 Proto 结构体中的 Upvaldesc* upvalues 字段,解析器解析 Lua 代码的时候会生成这个 UpValue 描述信息,并用于生成指令,而执行器运行的时候可以通过该描述信息方便快速地构建出真正的 UpValue 数组。
至此,我们知道了函数拥有 UpValue,有常量,有局部变量。外部数据 UpValue 也讲完,内部数据也讲完。接下来,我们开始学习函数运行的逻辑指令相关内容。
函数逻辑指令存储于函数原型 Proto 结构体中,这些函数逻辑是由一行行的 Lua 代码构成的,代码会被解析器翻译成 Lua 虚拟机能识别的指令,我们把这些指令称为 "OpCode",也叫 "操作码"。Proto 结构体存储 OpCode 使用的是下图中红框部分字段,见源码《lobject.h》Proto 结构体:
至此,我们可以简单提前说一下 Lua 虚拟机的功能了,本质上来看,Lua 虚拟机的工作,就是为当前函数(或者当前一段 OpCode 数组)准备好数据,然后有序执行 OpCode 指令。
对 OpCode 有了一定的认识了,接下来我们要补充一个 OpCode 相关的 Lua 闭包相关的内容,就是 Lua 闭包的运行环境。
一个 Lua 文件在载入的时候会先创建出一个最顶层(Top level)的 Lua 闭包,该闭包默认带有一个 UpValue,这个 UpValue 的变量名为 "_ENV",它指向 Lua 虚拟机的全局变量表,即_G 表,可以理解为_G 表即为当前 Lua 文件中代码的运行环境 (env)。事实上,每一个 Lua 闭包它们第一个 UpValue 值都是_ENV。
ENV 的定义在我们之前提到的解析器相关函数 mainfunc 中,见源码《lparser.c》:
如果想要设置这个载入后的初始运行环境不使用默认的 _G 表,除了直接在该文件代码中重新赋值_ENV 变量这种粗暴且不推荐的方式以外,通常是通过我们前面提到的加载 Lua 文件函数或加载 Lua 字符串代码函数传入 env 参数(Table 类型),就可以用自定义的 Table 作为当前 Lua 闭包的全局变量环境了,env 参数为上面两个函数的最末尾一个参数,'[' 与 ']' 字符中的内容表示参数可选,函数的定义摘自 Lua5.4 官网文档:
所以我们可以在 Lua 代码通过 _ENV 访问当前环境:
在 Lua 的旧版本中,变量的查询最多会分为 3 步:1)先从函数局部变量中进行查找;2)找不到的话就从 UpValue 中查找;3)还找不到就从全局环境默认 _G 表查找。而在 Lua5.4 中,把 UpValue 与全局 _G 表的查询统一为 UpValue 查询,并把一些操作判断提前到了解析器解析阶段进行,例如函数内部使用的某个 UpVaue 变量在代码解析的时候就可以通过 UpValue 描述信息知道存储于 Lua 闭包 upvals 数组的哪个下标位置,在执行器运行的时候只需要直接在数组拿取对应下标的这个 UpValue 数据即可。
从 OpCode 的层面来看,Lua 除了支持通过一个 UpValue 数组下标访问一个 UpValue 变量,在把 _G 表合并到 UpValue 之后,Lua 为此实现了通过一个字符串 key 值从某个 Table 类型的 UpValue 中查询变量的操作。
至此,我们了解了 Lua 闭包的结构与运行环境,以及 OpCode 的基本概念。接下来,我们将深入学习 OpCode,掌握 OpCode 就掌握了整个 Lua 虚拟机数据与逻辑的流向。