脚本虚拟机:scrProgram / scrThread / 指令集(rage/script)
本页解释 RAGE 脚本运行时的核心:编译后的 SanScript 程序在内存里长什么样(scrProgram)、一个脚本实例如何被执行(scrThread 与它的解释循环)、字节码指令集、全局变量块、WAIT 等阻塞语义、错误处理与调试设施。GTAV 中所有任务、多人模式、HUD 逻辑等“脚本内容”最终都在这台栈式虚拟机上运行,游戏 C++ 通过数千个 native 命令向它暴露能力。
背景与相关页面#
- 游戏侧如何创建线程、每帧驱动 VM、管理脚本拥有的实体与资源:script-handlers-and-threads.md。
- native 命令的注册宏、参数封送与
game/script/commands_*.cpp分类:natives-and-commands.md。 .sc源码如何被编译成本页描述的字节码:script-compiler-and-toolchain.md。- 脚本内容本身(
script/dev_ng/*):../15-script-content/script-tree-overview.md。 scr_scaleform(Scaleform 的脚本绑定)归前端页:../18-frontend-ui/scaleform-integration.md。- 资源(
pgBase/datResource)与流式加载:../02-rage-core/paging-and-resources.md、../04-rage-framework/streaming-engine.md。 - 防篡改相关机制只在本页做存在性说明,架构见 ../23-debug-security/security-overview.md。
术语:program = 一个编译后的脚本(scrProgram,内容只读、可被多个线程共享);thread = 程序的一个执行实例(scrThread,有自己的栈、PC、静态变量副本);native/command = 由 C++ 实现、脚本通过 OP_NATIVE 调用的函数;global block = 所有脚本共享的全局变量分区。
相关源码文件#
| 文件 | 作用 |
|---|---|
rage/script/src/script/program.h/.cpp | scrProgram:程序格式、分页、.sco 读写、资源化构造、全局块分配、程序注册表、引用计数 |
rage/script/src/script/thread.h/.cpp | scrThread:线程/栈池、解释器两套核心、内置命令、native 注册表、调试/剖析 |
rage/script/src/script/opcodes.h、opcode.h | 指令表(X-macro)与 scrOpcode 枚举 |
rage/script/src/script/value.h | scrValue 联合体、scrProgramId |
rage/script/src/script/hash.h/.cpp | scrComputeHash(程序名/旧式命令名哈希)、开放寻址 scrHash 模板 |
rage/script/src/script/signature.h/.cpp | scrSignature:native 的类型签名(调试用) |
rage/script/src/script/wrapper.h、varargs.h、array.h | native 封送模板与注册宏(详见 natives-and-commands.md) |
rage/script/src/script/bankscript.cpp | RAG widget 相关 native(WIDGET_EXISTS 等) |
rage/script/src/scriptgui/debugger.*、widgeteditor.* | RAG 内的脚本调试器 scrDebugger |
rage/script/src/scr_grcore/grcore.cpp | 示例用的即时模式绘制 native(GRC_*) |
rage/script/src/script/threadrun*.cpp、threadrunasm.s、threadtest*.cpp、scriptrun.cpp、register_secure.cpp、update_secure.cpp、scannative.cpp、csupport*.h | 遗留/独立工具或未启用功能,见第 14 节 |
当前库工程只编译 program.cpp、thread.cpp、hash.cpp、bankscript.cpp、signature.cpp 五个源文件(其余为头文件和文档) src/dev_ng/rage/script/src/script/makefile.txt:1-29,与生成的 VS 工程一致 src/dev_ng/rage/script/src/script/script_2012.vcxproj.filters:25-29。也就是说,threadrun.cpp 等文件不参与当前构建,真正的解释器全部在 thread.cpp 里(该文件自己也注释说 threadrun.cpp 曾经是独立文件,后因脆弱被并回 src/dev_ng/rage/script/src/script/thread.cpp:4608-4609)。
1. 总体数据流#
图中证据:编译器先 scr_parse() 再 scrNode::GetRoot()->Emit(),然后 scrProgram::Init + scrProgram::Save 写出 .sco src/dev_ng/rage/script/tools/scriptcompiler/sc.cpp:383-454;scriptrc 用 scrProgram::Load 读 .sco,再经 DeclareStruct 和 pgRscBuilder::SaveBuild(...,"#sc",RORC_VERSION) 写平台资源 src/dev_ng/rage/script/tools/scriptrc/scriptrc.cpp:59-84;游戏侧脚本仓库 CStreamedScripts 是 fwAssetRscStore<scrProgram,...>,版本号即 scrProgram::RORC_VERSION src/dev_ng/game/script/streamedscripts.cpp:42-45;资源构造函数里调用 scrThread::Validate(*this) src/dev_ng/rage/script/src/script/program.cpp:378,Validate 把 native 表中的哈希替换为函数指针 src/dev_ng/rage/script/src/script/thread.cpp:2164-2180;线程创建/执行与 native 调用见下文第 5–6 节。游戏代码中没有调用 scrProgram::Load(.sco 路径只被编译器、scriptrc、scannative 等工具使用;通过在 game/ 下检索 scrProgram::Load( 无结果得出)。
2. scrValue:VM 的基本存储单元#
scrValue 是一个联合体:Int/Uns/Float,以及 String(const char*)、Reference(scrValue*)、Any(size_t) src/dev_ng/rage/script/src/script/value.h:19-45。
- 尺寸:编译期断言在
SCRVALUE_OFFSETS==0时为 8 字节(32 位平台 4 字节)src/dev_ng/rage/script/src/script/value.h:57;SCRVALUE_OFFSETS(把指针压成 32 位偏移)被硬编码关闭src/dev_ng/rage/script/src/script/value.h:10。因此本代际(x64/PS4/XB1 等)每个脚本“字”都是 8 字节,脚本中INT/FLOAT只用低 32 位。 .sco中 static/global 初值以 32 位存储,加载时在字节序转换后零扩展为 64 位(保证 NULL 仍为 0)src/dev_ng/rage/script/src/script/program.cpp:763-772、src/dev_ng/rage/script/src/script/program.cpp:880-890。ValueType枚举(UNKNOWN, BOOL, INT, FLOAT, VECTOR, TEXT_LABEL, STRING, OBJECT)被scrSignature和调试器使用;VarArgType(VA_INT/VA_FLOAT/VA_STRINGPTR/VA_VECTOR)用于 VARARGS nativesrc/dev_ng/rage/script/src/script/value.h:21-26。- 文本标签最大 64 字节(
scrMaxTextLabelSize)src/dev_ng/rage/script/src/script/value.h:55;C++ 侧scrTextLabel7/15/23/31/63按sizeof(scrValue)倍数定义src/dev_ng/rage/script/src/script/thread.h:74-78。 scrVector在 64 位下是x,xPad,y,yPad,z,zPad(每分量占一个 8 字节槽)src/dev_ng/rage/script/src/script/thread.h:104-108。scrProgramId只是一个枚举包装的 32 位哈希src/dev_ng/rage/script/src/script/value.h:8。
3. scrProgram:程序格式#
3.1 内存布局#
scrProgram 继承 pgBase,可直接作为 RAGE 资源原地加载 src/dev_ng/rage/script/src/script/program.h:41-55。字段(注释中的偏移是 32 位时代遗留)src/dev_ng/rage/script/src/script/program.h:179-196:
| 字段 | 含义 |
|---|---|
u8 **Table | 代码页表;每页 scrPageSize 字节 |
u32 GlobalsHash | 本程序所依赖全局块的“签名”(初值数据哈希) |
u32 OpcodeSize | 字节码总长度 |
u32 ArgStructSize | 启动参数结构大小(单位:VM 字) |
u32 StaticSize / scrValue *Statics | 静态变量个数与初值 |
u32 GlobalSizeAndBlock / scrValue **GlobalsTable | 低 18 位为全局变量个数、高位为块号;初值按页存放 |
u32 NativeSize / u64 *Natives | native 表:加载时是 64 位哈希,Validate 后原地改写为函数指针 |
u32 ProcCount / const char **ProcNames | 子程序名表(仅剖析/Rocky 使用时生成) |
u32 HashCode | scrComputeHash(脚本名),即 scrProgramId |
u32 RefCount | 引用计数 |
ConstString ScriptName | 脚本名 |
const char **StringHeaps / u32 StringHeapSize | 字符串常量堆,分页存放 |
atMap<s32,bool> *m_programBreakpoints | 程序级断点(不序列化) |
3.2 分页与“指令不跨页”约束#
代码、字符串堆、全局初值都按 2^14 分页:scrPageShift = 14、scrStringShift = 14、scrGlobalsPageShift = 14 src/dev_ng/rage/script/src/script/program.h:26-39。代码页 16 KB,字符串页 16 KB,全局初值页 16384 个 scrValue(注释说明分页是为了避免为全局初值分配超大连续块)。operator[] 通过 Table[index >> scrPageShift][index & scrPageMask] 访问字节码 src/dev_ng/rage/script/src/script/program.h:69-77;最后一页按实际剩余长度分配 src/dev_ng/rage/script/src/script/program.cpp:277-282。
分页带来一个关键不变量:一条指令不能跨越页边界,并且除跳转类、CALL、CALLINDIRECT、LEAVE、SWITCH、THROW、NOP 外,指令不能紧贴页尾结束(宏 INSN_THAT_CAN_BE_AT_END_OF_PAGE) src/dev_ng/rage/script/src/script/opcode.h:24。编译器在 EmitOp 中检测剩余空间,不够时插入 J 跳过或用 NOP 填充 src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:252-275;Save 和 SwapForPlatform 都对此做断言 src/dev_ng/rage/script/src/script/program.cpp:980-981、src/dev_ng/rage/script/src/script/thread.cpp:2316。解释器因此只需在“控制流转移”时重新定位页(SET_PC),顺序执行时直接 ++pc 即可 src/dev_ng/rage/script/src/script/thread.cpp:4641-4649。这表明分页的目的是让流式资源不必是一大块连续内存,同时保持热路径无额外开销(推断,依据为上面的宏设计)。
3.3 .sco 文件格式(工具路径)#
.sco 头为 12 个 u32:Magic、GlobalsHash、CompressedSize、EncryptionKey、OpcodeSize、StaticSize、GlobalSize、ArgStructSize、NativeSize、StringHeapSize、GlobalsBlock、Pad src/dev_ng/rage/script/src/script/program.cpp:722-738;魔数为 MAKE_MAGIC_NUMBER('S','C','R',12) src/dev_ng/rage/script/src/script/program.cpp:740。
scrProgram::Load(filename, scriptname) 的流程 src/dev_ng/rage/script/src/script/program.cpp:775-916:
- 用脚本基名算
scrComputeHash,若注册表里已有同名程序则仅AddRef并返回(同名程序全局唯一)。 - 可选地按
-scriptproject改写路径,ASSET.Open(filename,"sco")。 - 读头、校验魔数(不符则报“旧版本格式”并返回 0)。
- 构造空
scrProgram(按头里的尺寸分配页);若头中EncryptionKey非零,对压缩体做 AES 解密(只描述存在,不涉及密钥)。 - zlib 依次解压到:各代码页 → 各字符串页 → native 表(
NativeSize*8) → statics(StaticSize*4) → 各全局初值页(每项 4 字节)。 - 大端转本机字节序;64 位下把 statics/globals 零扩展。
- 若带全局块则
SetGlobals()(见第 4 节);scrThread::Validate检查 native;scrThread::SwapForPlatform把指令立即数转成本机字节序(资源编译器路径在需要字节交换时跳过)。 - 插入注册表;若程序带
GlobalsHash但对应全局块尚未被任何“GLOBALS TRUE”脚本建立,则Quitf("...requires globals that are not available...")。
scrProgram::Save(仅 __WIN32PC,即编译器端)做反向操作:把 opcode、字符串堆、native、statics、指定全局块拼成一个缓冲,Z_BEST_COMPRESSION 压缩后加密写出 src/dev_ng/rage/script/src/script/program.cpp:942-1089。写出前它会打乱 native 表顺序并同步修正每条 OP_NATIVE 的索引操作数 src/dev_ng/rage/script/src/script/program.cpp:952-983(这属于防篡改范畴,此处只记录流程)。
3.4 资源化格式与运行时加载(游戏路径)#
游戏实际加载的是平台资源:PC x64 为 ysc src/dev_ng/rage/framework/src/fwsys/fileExts.h:102、PC 32 位 wsc src/dev_ng/rage/framework/src/fwsys/fileExts.h:135、Durango dsc src/dev_ng/rage/framework/src/fwsys/fileExts.h:176、Orbis osc src/dev_ng/rage/framework/src/fwsys/fileExts.h:210、Xenon xsc src/dev_ng/rage/framework/src/fwsys/fileExts.h:244、PS3 csc src/dev_ng/rage/framework/src/fwsys/fileExts.h:278,平台无关为 sco src/dev_ng/rage/framework/src/fwsys/fileExts.h:351。资源版本 RORC_VERSION = 12 src/dev_ng/rage/script/src/script/program.h:45;CStreamedScripts 以 pgStreamer::ENCRYPTED 方式读取 src/dev_ng/game/script/streamedscripts.cpp:47-50。
- 资源编译端:
__RESOURCECOMPILER下的拷贝构造复制所有页src/dev_ng/rage/script/src/script/program.cpp:468-516,DeclareStruct对页指针、native、statics、globals 做字节交换并声明结构布局src/dev_ng/rage/script/src/script/program.cpp:426-465。 - 运行时:
scrProgram(datResource&)修复所有指针(Table、每页、Statics、Natives、GlobalsTable、StringHeaps),对 native 表做与DeclareStruct对称的逆变换src/dev_ng/rage/script/src/script/program.cpp:337-360。非碎片整理调用时:检查全局签名、++s_AllRefCounts、SetGlobals()、scrThread::Validate、锁定字符串页以便其余页仍可被碎片整理、插入注册表src/dev_ng/rage/script/src/script/program.cpp:362-393。碎片整理(defrag)调用时只修复ProcNamessrc/dev_ng/rage/script/src/script/program.cpp:394-401。
锁定字符串页的原因:VM 栈上会保存指向字符串堆的裸指针(OP_STRING 把偏移转换为 const char*,见 6.3),如果字符串页被移动这些指针会悬空(推断;代码注释只说明“只锁定这些页以便其余部分仍可碎片整理”)。代码页不锁定,因为线程只保存 PC 偏移,每次 Run 入口都会通过 Table 重新定位页(SET_PC(ser->m_PC)) src/dev_ng/rage/script/src/script/thread.cpp:4732。
3.5 程序注册表 scrProgramRegistry#
所有已加载程序登记在一个静态哈希表 s_ProgHash 中(注释说明不用通用 scrHash 是因为它不支持碎片整理) src/dev_ng/rage/script/src/script/program.cpp:78-241:
- 32 个桶,最多 176 个槽,槽 0 保留作空闲链表头,因此同时驻留的程序上限为 175 个;超过 160 个时
VerifyLists发出警告断言src/dev_ng/rage/script/src/script/program.cpp:229-236、src/dev_ng/rage/script/src/script/program.cpp:221-225。 - 所有操作在
sm_Token临界区下进行(SYS_CS_SYNC),流式线程与主线程可并发插入/查找src/dev_ng/rage/script/src/script/program.cpp:94-109。 GetProgram(progId)即s_ProgHash.Lookupsrc/dev_ng/rage/script/src/script/program.cpp:1141-1143。
3.6 引用计数与生命周期#
AddRef同时递增本程序RefCount与全局s_AllRefCountssrc/dev_ng/rage/script/src/script/program.cpp:663-666。Release递减两者;当s_AllRefCounts归零时释放所有 64 个全局块并清空签名,即“最后一个程序卸载时重置所有全局变量”;本程序RefCount归零则delete thissrc/dev_ng/rage/script/src/script/program.cpp:668-719。- 析构从注册表移除并释放所有页
src/dev_ng/rage/script/src/script/program.cpp:408-424。 scrThread::CreateThread(progId)成功时AddRefsrc/dev_ng/rage/script/src/script/thread.cpp:3150-3153,scrThread::Kill时Releasesrc/dev_ng/rage/script/src/script/thread.cpp:3380-3382。游戏侧CStreamedScripts::GetNumRefs/RemoveRef把流式引用与程序引用合并判断是否可卸载(见 script-handlers-and-threads.md)。
3.7 哈希:scrComputeHash 与 scrHash#
scrComputeHash 是 Jenkins one-at-a-time 哈希:忽略大小写、\ 归一为 /、可去掉首尾引号,结果 <2 时加 2(0/1 保留为 EMPTY/DELETED) src/dev_ng/rage/script/src/script/hash.cpp:17-48。它用于:程序名 → scrProgramId(HashCode)、调试用命令名映射、编译器的 include 去重等。scrValidateHash 在断言构建下校验 SCRHASH(str,code) 宏:若 code 只有 32 位则必须等于 scrComputeHash(str),若高位非零则视为随机数直接放行 src/dev_ng/rage/script/src/script/hash.cpp:53-62——这对应 native 标识从“名字哈希”演进为“64 位随机数”(见 natives-and-commands.md)。scrHash<T> 是开放寻址(双重哈希式步长)表模板 src/dev_ng/rage/script/src/script/hash.h:15-80。
注意程序 ID 与游戏层常用的 atStringHash 不同:GtaThread::Reset 另外计算 m_HashOfScriptName = atHashString::ComputeHash(GetScriptName()) src/dev_ng/game/script/gta_thread.cpp:127-131,而 CTheScripts::GtaStartNewThreadWithNameHashOverride 的注释也指出 IsCompiledAndResident 用的是另一套哈希 src/dev_ng/game/script/script.cpp:504-505。
4. 全局变量块(global blocks)#
4.1 分区与地址编码#
- 共有
MAX_GLOBAL_BLOCKS = 64个块;24 位全局地址中高 6 位是块号、低 18 位(GLOBAL_SIZE_MASK)是块内偏移src/dev_ng/rage/script/src/script/program.h:47-51。每块上限SCRIPT_GLOBAL_COUNT = 1<<18个scrValue(超出则Quitf)src/dev_ng/rage/script/src/script/program.cpp:37、src/dev_ng/rage/script/src/script/program.cpp:597-598。按 8 字节scrValue,单块最大 2 MB。 OP_GLOBAL_U16*只能访问块 0 的前 64K 个变量;OP_GLOBAL_U24*解出块号与偏移,若该块未驻留(globals[block]==NULL)则 fault “Global block %u/%u not resident”src/dev_ng/rage/script/src/script/thread.cpp:5094-5131。- 编译器语法:
GLOBALS [块号]…ENDGLOBALS声明块(不可嵌套、同块不可重复),GLOBALS [块号] TRUE指定本脚本负责保存该块的初值(设置g_SaveGlobalsBlock)src/dev_ng/rage/script/tools/scriptcompiler/scr.y:585-602、src/dev_ng/rage/script/tools/scriptcompiler/scr.y:931、src/dev_ng/rage/script/tools/scriptcompiler/scr.y:936-939。只有该脚本的.sco携带块初值(Save中hdr.GlobalSize仅在globalsBlock != -1时非零)src/dev_ng/rage/script/src/script/program.cpp:988。
文档与实现差异:
script.dtx仍写着“整个游戏只能有一个 GLOBALS 块”src/dev_ng/rage/script/src/script/script.dtx:39-47,而代码已支持 64 个块。
4.2 分配:scrProgram::SetGlobals#
当一个带 GlobalSizeAndBlock 的程序加载时调用 SetGlobals src/dev_ng/rage/script/src/script/program.cpp:587-656:
- 断言该块尚无签名、尚未分配(同一块只能由一个脚本建立)。
- 按平台分配:
RSG_PC用sysMemVirtualAllocate;ENABLE_SCRIPT_GLOBALS_HEAP(RSG_ORBIS || RSG_DURANGOsrc/dev_ng/rage/script/src/script/program.h:22)从专用sysTinyHeap分配,失败Quitf("Out of ScriptGlobalMemory! Increase runtime tweakable!");其他平台rage_new。 - 把各初值页拷入连续内存,记录签名
s_GlobalsHashes[block]、关联脚本名,最后才发布sm_Globals[block]指针(注释强调 memcpy 完成后再赋值)。PC 上额外记录“块号 → 脚本名哈希”映射sm_GlobalBlockIndexToNameHashsrc/dev_ng/rage/script/src/script/program.cpp:648-651。
专用堆的大小来自配置池 "scrGlobals",在 CTheScripts::Init(INIT_CORE) 中通过 scrProgram::InitGlobalsHeap 建立 src/dev_ng/game/script/script.cpp:690-696、src/dev_ng/rage/script/src/script/program.cpp:541-553;会话关闭时断言所有块已释放(AssertGlobalsHeapEmpty) src/dev_ng/game/script/script.cpp:930-932。
4.3 全局签名校验#
签名 = 对块初值(每项截为 32 位)做 atDataHash src/dev_ng/rage/script/src/script/program.cpp:520-538。编译器为每个块计算签名并写入引用它的每个脚本 src/dev_ng/rage/script/tools/scriptcompiler/scr.y:533-543。运行时构造程序时,若程序签名与该块已登记的签名不一致,默认 Quitf,AllowMismatchedGlobals(true) 时降级为 Errorf src/dev_ng/rage/script/src/script/program.cpp:314-324、src/dev_ng/rage/script/src/script/program.cpp:365-373。这保证所有脚本对全局布局的认识一致——全局变量没有符号信息,完全靠编译期分配的固定偏移访问。
4.4 调试用全局符号表#
编译器 -globals 选项可输出 .sgv;scrThread::LoadGlobalVariableInfo 读取 'sgv0' 魔数文件为 {BasicType, Offset, Hash} 数组,供控制台按名字查找全局变量 src/dev_ng/rage/script/src/script/thread.cpp:4532-4562、src/dev_ng/rage/script/src/script/thread.h:183-187。
5. scrThread:执行实例#
5.1 状态与可序列化状态块#
State { RUNNING, BLOCKED, ABORTED, HALTED }:RUNNING 表示(在调试核心中)用完了本帧指令配额;BLOCKED 表示调用了本帧未完成的函数(典型是 WAIT);ABORTED 表示致命错误或执行结束,不会再运行;HALTED 用于调试器 src/dev_ng/rage/script/src/script/thread.h:146-151。优先级 THREAD_PRIO_HIGHEST/NORMAL/LOWEST 以及 THREAD_PRIO_MANUAL_UPDATE = 100 src/dev_ng/rage/script/src/script/thread.h:152。
线程的全部可存档状态放在无指针的 Serialized 结构中(设计意图:简化存读档) src/dev_ng/rage/script/src/script/thread.h:136-181:
| 字段 | 含义 |
|---|---|
m_ThreadId | 单调递增的线程 ID(0 = 空闲槽) |
m_Prog | 程序哈希(而非指针,便于存读档) |
m_State / m_PC / m_FP / m_SP | VM 寄存器(均为索引/偏移) |
m_TimerA/B | TIMERA/TIMERB(毫秒,浮点累加) |
m_Wait | WAIT 剩余时间(秒) |
m_MinPC/m_MaxPC | 调试单步区间 |
m_TLS[4] | 供阻塞 native 保存状态的线程局部槽 |
m_StackSize | 栈容量(VM 字) |
m_CatchPC/FP/SP | 唯一一层 CATCH 快照 |
m_Priority | 调度优先级 |
m_CallDepth / m_CallStack[16] | 最近 16 层调用的返回点(用于栈回溯/剖析) |
GetSerializedStateSize 在非 FINAL 构建中排除调用栈部分 src/dev_ng/rage/script/src/script/thread.h:540。实例字段还包括 m_Stack、m_iInstructionCount、arg 结构偏移、m_AbortReason、m_ScriptName[64] 等 src/dev_ng/rage/script/src/script/thread.h:745-776。“当前线程”分两种:s_CurrentThread 是 __THREAD(每 CPU 线程)变量,s_CurrentThreadGlobal 是跨线程全局 src/dev_ng/rage/script/src/script/thread.cpp:193-195;native 通过 scrThread::GetActiveThread() 取得调用者 src/dev_ng/rage/script/src/script/thread.cpp:488-498。
5.2 线程池与栈池#
- 线程对象和栈都是预分配的:
AllocateThreads(count, stackSize)创建count个线程对象并预留count*2个栈描述符src/dev_ng/rage/script/src/script/thread.cpp:2460-2467;AddThreadStack(count, size)追加指定尺寸的栈src/dev_ng/rage/script/src/script/thread.cpp:2450-2457。 AllocateStack(size)只接受尺寸完全相等的空闲栈;找不到就断言“检查 gameconfig.xml 与 stack_sizes.sch 是否同步”src/dev_ng/rage/script/src/script/thread.cpp:2914-2939。注释说明栈的回收被尽量推迟,以便调试器还能查看已中止线程的栈。- GTAV 中线程/栈数量与尺寸来自
gameconfig.xml的CConfigScriptStackSizes,由GtaThread::AllocateThreads读取(见 script-handlers-and-threads.md)。默认栈尺寸常量c_DefaultStackSize = 512仅用于 RAGE 默认参数src/dev_ng/rage/script/src/script/thread.h:142。
5.3 栈布局#
线程栈是一个 scrValue 数组,从低到高为:静态变量区(含启动参数结构)→ 主函数帧 → 调用帧……
Reset把程序的 statics 初值拷到m_Stack[0..StaticSize),启动参数结构位于 statics 区末尾(m_argStructOffset = staticSize - progArgStructSize),并把 SP/FP 置于 statics 之后,再压入返回地址 0src/dev_ng/rage/script/src/script/thread.cpp:2848-2864。statics 太多(>= m_StackSize)或参数尺寸不匹配时Faultsrc/dev_ng/rage/script/src/script/thread.cpp:2813-2828。- 因此
OP_STATIC_*直接以stack + imm寻址src/dev_ng/rage/script/src/script/thread.cpp:5027-5029,OP_LOCAL_*以fp + imm寻址src/dev_ng/rage/script/src/script/thread.cpp:5023-5025。每个线程拥有自己的 statics 副本,这就是script.dtx所说“块外变量对每个脚本副本唯一,类似 C++ 成员变量”src/dev_ng/rage/script/src/script/script.dtx:49-53。 - 调用帧:调用者压入参数,
OP_CALL压入返回 PC,OP_ENTER压入调用者 FP 并让fp指向第一个参数,再按 u16 操作数把帧内局部区清零并抬高 SP;OP_LEAVE把 SP 复位到fp + paramCount + 2处、取回保存的 FP 与返回地址、丢弃参数并把返回值拷到新栈顶src/dev_ng/rage/script/src/script/thread.cpp:4896-4956(调试核心的等价实现src/dev_ng/rage/script/src/script/thread.cpp:1344-1425)。主脚本的“返回地址”是Reset压入的 0,所以主函数LEAVE后 PC 为 0 即线程结束(ABORTED,调试核心记录 "Reached end of program.")。 - 数组:引用指向的第一个
scrValue是元素个数,元素从r + 1开始,步长由指令立即数给出;越界检查在两套核心里都始终开启src/dev_ng/rage/script/src/script/thread.cpp:4984-5021。C++ 侧通过scrArray::GetElements/GetCount访问同样的布局src/dev_ng/rage/script/src/script/array.h:42-60。
5.4 生命周期#
证据:CreateThread(progId) 先回收尺寸相同的已中止线程,再找 m_ThreadId==0 的空槽、分配栈、清 TLS、Reset、AddRef,最后分配 ++sm_ThreadId src/dev_ng/rage/script/src/script/thread.cpp:3132-3190;Kill 若作用于当前正在执行的线程,只把状态设为 ABORTED("via scrThread::Kill on myself"),否则释放程序、归还栈并清零 ID src/dev_ng/rage/script/src/script/thread.cpp:3368-3397;SetThreadBlockState 只在 RUNNING/BLOCKED 间切换 src/dev_ng/rage/script/src/script/thread.h:499-505;Continue/Pause/SetStepRegion 用于 HALTED src/dev_ng/rage/script/src/script/thread.cpp:2006-2034。“自杀”线程留在 ABORTED 状态,需要别人再 Kill 一次才真正释放——GTAV 每帧调用 GtaThread::KillAllAbortedScriptThreads 完成这一步(见 script-handlers-and-threads.md)。
6. 解释器#
6.1 两套解释核心与构建配置#
thread.cpp 中有两个 Run:
- 调试核心:成员函数
scrThread::State scrThread::Run(int insnCount)内的大switch,支持断点、单步、逐指令计数、引用合法性校验、native 追踪、剖析src/dev_ng/rage/script/src/script/thread.cpp:850-1953。 - 快速核心:静态函数
u32 scrThread::Run(scrValue *stack, scrValue **globals, const scrProgram &pt, Serialized *ser),把 SP/FP/PC 放在局部指针里,不计指令数src/dev_ng/rage/script/src/script/thread.cpp:4720-5303。
选择逻辑:SCRIPT_DEBUGGING = (__DEV || __BANK) src/dev_ng/rage/script/src/script/thread.h:16,SCRIPT_PROFILING = (__BANK && __OPTIMIZED) src/dev_ng/rage/script/src/script/thread.h:23,SMALL_SCRIPT_CORE = (!SCRIPT_DEBUGGING && !SCRIPT_PROFILING) src/dev_ng/rage/script/src/script/thread.cpp:675。
| 构建 | Run(int) 的行为 |
|---|---|
| 非 DEV 且非 BANK(含 Release/Profile/Final) | SMALL_SCRIPT_CORE 分支:设置当前线程后直接调用快速核心 src/dev_ng/rage/script/src/script/thread.cpp:885-906 |
| DEV 或 BANK | 默认走调试核心;仅当本线程无断点、未开脚本剖析、且命令行有 -fastscriptcore(PS3 上 SN Tuner 运行时亦然)才转快速核心 src/dev_ng/rage/script/src/script/thread.cpp:931-940、src/dev_ng/rage/script/src/script/thread.cpp:685 |
快速核心在 RSG_ORBIS 上使用 computed-goto 线程化分派(256 项 jumpTable,未用 opcode 指向 LABEL_default),其他平台用 switch src/dev_ng/rage/script/src/script/thread.cpp:4630、src/dev_ng/rage/script/src/script/thread.cpp:4752-4795。调试核心在非 SCRIPT_DEBUGGING 时用 goto DISPATCH 代替 while 循环(GOTO_HACK) src/dev_ng/rage/script/src/script/thread.cpp:987-996。在 __DEV || __BANK 的快速核心中,VALIDATE_PC/VALIDATE_PTR 会拒绝小于 4 的跳转目标和数值小于 16384 的引用 src/dev_ng/rage/script/src/script/thread.cpp:4632-4639。
6.2 指令编码与指令集#
指令表以 X-macro OP(name, size, operandFormat) 定义在 opcodes.h,opcode.h 展开为 scrOpcode 枚举 src/dev_ng/rage/script/src/script/opcode.h:13-20。共 131 条(0–130),编译器以 CompileTimeAssert(OP_FIRST_INVALID == 131) 锁定 src/dev_ng/rage/script/tools/scriptcompiler/sc.cpp:28。操作数格式字母:b=u8、s=s16、h=u16、a=24 位地址、d=u32、f=float、$=计数字符串、R=相对跳转、S=switch 表 src/dev_ng/rage/script/src/script/opcodes.h:1。尺寸为 0 的两条变长指令:ENTER(5 + nameLen)与 SWITCH(2 + 6*count) src/dev_ng/rage/script/src/script/thread.cpp:2322-2340。
| 类别 | 指令(编号) | 说明 / 证据 |
|---|---|---|
| 空操作 | NOP(0) | 页尾填充 src/dev_ng/rage/script/src/script/opcodes.h:3 |
| 整数算术/比较 | IADD ISUB IMUL IDIV IMOD INOT INEG(1–7)、IEQ INE IGT IGE ILT ILE(8–13) | 除数为 0 时 IDIV/IMOD 保持左操作数不变而非抛错 src/dev_ng/rage/script/src/script/thread.cpp:4817-4830 |
| 浮点算术/比较 | FADD … FNEG(14–19)、FEQ … FLE(20–25) | FDIV/FMOD 除数位模式为 0 时跳过;FNEG 翻转符号位 src/dev_ng/rage/script/src/script/thread.cpp:4832-4844 |
| 向量 | VADD VSUB VMUL VDIV VNEG(26–30) | 三个连续槽逐分量运算 src/dev_ng/rage/script/src/script/thread.cpp:4846-4850 |
| 位运算 | IAND IOR IXOR(31–33)、IS_BIT_SET(130) | IS_BIT_SET 原是 native,编译器特判为 opcode 以减少开销 src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:2258-2264 |
| 转换 | I2F F2I F2V(34–36) | F2V 把一个 float 复制成三槽向量 src/dev_ng/rage/script/src/script/thread.cpp:4856-4858 |
| 常量/栈 | PUSH_CONST_U8/U8_U8/U8_U8_U8/U32/F(37–41)、DUP DROP(42–43)、PUSH_CONST_S16(67)、PUSH_CONST_U24(100)、PUSH_CONST_M1…7(112–120)、PUSH_CONST_FM1…F7(121–129) | 多数是编译器窥孔优化的合并形式 src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:198-245 |
| native 调用 | NATIVE(44) | 见 6.3 |
| 帧/调用 | ENTER LEAVE(45–46)、CALL(93)、CALLINDIRECT(111) | 见 5.3 / 6.3 |
| 间接存取 | LOAD STORE STORE_REV(47–49)、LOAD_N STORE_N(50–51) | STORE_N 反序写入以匹配求值顺序 src/dev_ng/rage/script/src/script/thread.cpp:4958-4982 |
| 数组 | ARRAY_U8[_LOAD/_STORE](52–54)、ARRAY_U16…(73–75) | 越界 fault;_LOAD/_STORE 变体紧随基础版是硬性约定 src/dev_ng/rage/script/src/script/opcodes.h:72-77 |
| 局部/静态/全局 | LOCAL_U8/U16…、STATIC_U8/U16/U24…、GLOBAL_U16/U24… | 取地址/加载/存储三变体 |
| 偏移 | IADD_U8 IMUL_U8 IOFFSET IOFFSET_U8…(61–66)、IADD_S16 IMUL_S16 IOFFSET_S16…(68–72) | 结构字段访问(IOFFSET 以 sizeof(scrValue) 为单位) src/dev_ng/rage/script/src/script/thread.cpp:5031-5045 |
| 跳转 | J JZ(85–86)、IEQ_JZ … ILE_JZ(87–92) | 有符号 16 位相对偏移,从下一条指令起算 src/dev_ng/rage/script/src/script/opcodes.h:120-128 |
| 多路分支 | SWITCH(101) | u8 个数 + (u32 值, u16 偏移) 线性查找,未命中则落到编译器紧随其后生成的 J src/dev_ng/rage/script/src/script/opcodes.h:142-147、src/dev_ng/rage/script/src/script/thread.cpp:5145-5162 |
| 字符串 | STRING(102)、STRINGHASH(103)、TEXT_LABEL_ASSIGN_STRING/INT、TEXT_LABEL_APPEND_STRING/INT(104–107)、TEXT_LABEL_COPY(108) | 见 6.3 |
| 异常 | CATCH THROW(109–110) | 见第 9 节 |
指令集分段有硬性顺序要求:16 位立即数指令(67–92)与 24 位立即数指令(93–100)必须连续,因为 SwapForPlatform 用编号区间判断如何交换字节 src/dev_ng/rage/script/src/script/opcodes.h:95-140、src/dev_ng/rage/script/src/script/thread.cpp:2261-2320。.sco 中立即数是大端,SwapForPlatform 在小端机器上就地转换;24 位立即数在快速核心中用一次 32 位非对齐读取再移位得到(大端平台 & 0xFFFFFF,小端 >> 8) src/dev_ng/rage/script/src/script/thread.cpp:4620-4628。opcode.h 的注释提醒新增 opcode 时须同步 Run 与反汇编器 src/dev_ng/rage/script/src/script/opcode.h:7-12。
6.3 关键指令语义#
OP_NATIVE(4 字节):第一个操作数字节低 2 位为返回值槽数、高 6 位为参数槽数,随后 16 位(大端)为本程序 native 表索引 src/dev_ng/rage/script/src/script/thread.cpp:4870-4877;编译器端对应 Emit8(returnSize | (paramSize << 2)) 并断言 returnSize <= 3、paramSize <= 63 src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:2250-2277。执行步骤(快速核心):
- 把 PC(回退到本条 NATIVE 起始,
c_NativeInsnLength = 4)、FP、SP 写回Serialized,以便 native 阻塞或访问线程状态时一致src/dev_ng/rage/script/src/script/thread.cpp:4881-4883、src/dev_ng/rage/script/src/script/thread.h:144。 - 构造
scrThread::Info{ResultPtr, ParamCount, Params}:参数就在栈上,返回值原地覆盖第一个参数槽src/dev_ng/rage/script/src/script/thread.cpp:4884。 - 调用
(*cmd)(curInfo);若返回后线程状态不再是 RUNNING(BLOCKED/ABORTED),立即返回该状态(PC 仍指向这条 NATIVE,下帧会重新执行它)。 - 否则
CopyReferencedParametersOut(把Vector3&参数的临时副本写回脚本内存)并弹掉paramCount - returnSize个槽src/dev_ng/rage/script/src/script/thread.cpp:4885-4893、src/dev_ng/rage/script/src/script/thread.cpp:5308-5334。
调试核心额外支持 scrbreak 断点函数、-nativetrace 的 Log/DumpStack/Break 动作(依赖 s_SignatureHash 中的签名格式化参数)、剖析计时与数组尺寸监视 src/dev_ng/rage/script/src/script/thread.cpp:1147-1300。
CALL/CALLINDIRECT:CALL 带 24 位绝对代码地址,压入返回偏移后 SET_PC src/dev_ng/rage/script/src/script/thread.cpp:5134。函数指针在 VM 中就是代码地址整数,CALLINDIRECT 从栈顶取地址,0 地址 fault "uninitialized (zero) function pointer" src/dev_ng/rage/script/src/script/thread.cpp:5256-5265。24 位地址决定单个程序代码不超过 16 MB;编译器的程序缓冲上限 MAX_LEGIT_OPS = 11 MB src/dev_ng/rage/script/src/script/program.h:24。
ENTER 的名字字段:非 final 编译时,ENTER 后跟 nameLen 字节,其中前两字节是剖析槽位占位符、其后是以 NUL 结尾的子程序名 src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:1501-1532;GenerateProcNames 在加载后把占位符改写成名字表索引供剖析器使用 src/dev_ng/rage/script/src/script/thread.cpp:2216-2250。-final 构建省略名字(-forcename 可强制保留,选项描述明确标注其为“安全风险,仅本地测试”) src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:1499。
字符串:字符串字面量编译为“偏移 + OP_STRING”,运行时转换为指向字符串堆页的指针 src/dev_ng/rage/script/src/script/thread.cpp:5163-5169;STRINGHASH 对字符串做 atStringHash src/dev_ng/rage/script/src/script/thread.cpp:5287。TEXT_LABEL_* 以立即数给出目标缓冲容量,实现安全截断的赋值/追加/整数转换;TEXT_LABEL_COPY 按槽复制并强制末尾 NUL src/dev_ng/rage/script/src/script/thread.cpp:5171-5230。
6.4 加载期处理:Validate 与 SwapForPlatform#
scrThread::Validate(program)遍历 native 表,用s_CommandHash.Lookup(hash)查找处理函数;查不到则填入InvalidNative(执行时Quitf),记录错误,并在非 FINAL 弹出“REPORT THIS TO CODERS!”消息框;找到的函数指针原地写回Natives[n]src/dev_ng/rage/script/src/script/thread.cpp:2155-2213。因此一个使用了未注册 native 的脚本仍能加载,但执行到那条指令时崩溃。- 开启
SCRIPT_PROFILING时Validate还会两遍扫描生成ProcNamessrc/dev_ng/rage/script/src/script/thread.cpp:2183-2207。
7. 调度、指令预算与时间片#
scrThread::UpdateAll(insnCount) 是 RAGE 提供的统一调度入口 src/dev_ng/rage/script/src/script/thread.cpp:2402-2447:
insnCount == 0时替换为DEFAULT_INSTRUCTION_COUNT = 13000000src/dev_ng/rage/script/src/script/thread.h:25。- 按优先级 HIGHEST → NORMAL → LOWEST 三轮遍历线程数组,每个有效线程调用
Update(insnCount);THREAD_PRIO_MANUAL_UPDATE(100) 的线程不会被遍历到,必须由拥有者手动Update(例如暂停菜单脚本CScriptMenusrc/dev_ng/game/frontend/CScriptMenu.cpp:127-152)。 - 每个线程得到同一个
insnCount,即配额是每线程每帧,而非所有线程共享。头文件注释声称“across all threads”且“记录上次运行的线程以防饿死”src/dev_ng/rage/script/src/script/thread.h:328-335,与实现不符(实现总是从数组头开始、无轮转)。
scrThread::Update src/dev_ng/rage/script/src/script/thread.cpp:2342-2399:若全局 sm_IsBlocked(GTAV 落地页期间会设置 src/dev_ng/game/frontend/landing_page/LandingPageController.cpp:65)则直接返回 BLOCKED;否则按 sm_TimeStep*1000 累加 TIMERA/B(暂停时是否累加由 sm_UpdateTimersWhenPaused 决定,默认 true src/dev_ng/rage/script/src/script/thread.cpp:116-120),然后 Run(insnCount);调试构建下超过 1000 ms 打印栈回溯。
指令配额只在调试核心中生效:每条指令 --insnCount,耗尽时 fault "Ran over per-script instruction allocation for frame"(THREAD_INSTRUCTION_COUNT_OVERFLOW),接近上限(剩余 < 1/64)时逐条打印 PC 以帮助定位死循环,单次 Run 超过 1 ms 打印警告 src/dev_ng/rage/script/src/script/thread.cpp:1920-1946。快速核心不计指令,只在 !__FINAL && !THREADED_INTERPRETER 下保留一个 10^9 条的“脚本卡死”保护 src/dev_ng/rage/script/src/script/thread.cpp:4734-4736、src/dev_ng/rage/script/src/script/thread.cpp:4801-4804。结论:发行构建中脚本是协作式调度——一个不 WAIT 的脚本会一直运行直到阻塞、结束或出错,帧时间完全依赖脚本作者自觉 WAIT。GTAV 侧 -scriptInstructionLimit 参数说明写的默认值是 1,000,000 src/dev_ng/game/script/script.cpp:129,而实际传入 0 会被替换为 13,000,000;script.dtx 则写着“每帧 1000 条” src/dev_ng/rage/script/src/script/script.dtx:246-249——三处数值互不一致,以代码为准。
8. WAIT 与阻塞语义#
证据:Wait 的两分支实现 src/dev_ng/rage/script/src/script/thread.cpp:2675-2690;NATIVE 在状态非 RUNNING 时立即返回且 PC 指向本指令 src/dev_ng/rage/script/src/script/thread.cpp:4881-4887、调试核心显式 m_PC = pc - c_NativeInsnLength src/dev_ng/rage/script/src/script/thread.cpp:1282-1284;Run 入口只拒绝 ABORTED/HALTED,BLOCKED 线程照常进入 src/dev_ng/rage/script/src/script/thread.cpp:887-888。
要点:
- 阻塞 native 的通用模式是“首次调用设置状态并阻塞,后续帧被重复调用直到条件满足”,状态可存放在
m_TLS[4]src/dev_ng/rage/script/src/script/thread.h:274-280。WAIT 0因此恰好让出一帧(下一帧第一次重入就解除阻塞)。script.dtx也这样描述src/dev_ng/rage/script/src/script/script.dtx:241-244。 sm_TimeStep由 GTAV 每帧在UpdateAll前用fwTimer::GetTimeStep()设置src/dev_ng/game/script/script.cpp:1133-1136,默认 1/60src/dev_ng/rage/script/src/script/thread.cpp:116。WaitUnwarped/WaitUnpaused已实现src/dev_ng/rage/script/src/script/thread.cpp:2692-2726,但RegisterBuiltinCommands没有注册它们(见第 10 节),游戏中也未检索到对SetTimeStepUnwarped/SetIsPaused的调用。CancelWait()把m_Wait置 -1src/dev_ng/rage/script/src/script/thread.h:508-510,GTAV 后台脚本退出流程会用到(GtaThread::SetExitFlag)。START_NEW_SCRIPT*在没有空闲线程/栈时把调用者置为 BLOCKED(下帧重试),成功则返回新线程 ID 并把调用者 TLS 中非空槽继承给新线程src/dev_ng/rage/script/src/script/thread.cpp:2768-2786。
9. 错误与异常处理#
9.1 Fault 路径#
| 情形 | 调试核心 | 快速核心 |
|---|---|---|
| 数组越界 | Fault(pc,"Array overrun",THREAD_ARRAY_OVERFLOW) src/dev_ng/rage/script/src/script/thread.cpp:1527 | Fault("Array overrun, %u >= %u") src/dev_ng/rage/script/src/script/thread.cpp:4989-4990 |
| 栈溢出 | CheckForOverflow → THREAD_STACK_OVERFLOW src/dev_ng/rage/script/src/script/thread.cpp:458-485 | 仅 ENTER 检查 sp - stack >= StackSize - localCount src/dev_ng/rage/script/src/script/thread.cpp:4915-4916 |
| 非法引用(LOAD/STORE) | VALIDATE_REFERENCES 时 IsValidReference 检查栈/全局范围 src/dev_ng/rage/script/src/script/thread.cpp:1434-1460、src/dev_ng/rage/script/src/script/thread.cpp:2103-2111 | 仅 DEV/BANK 的 VALIDATE_PTR(<16384) |
| 全局块未驻留 | Fault src/dev_ng/rage/script/src/script/thread.cpp:1508-1514 | Fault src/dev_ng/rage/script/src/script/thread.cpp:5106-5107 |
| 指令配额耗尽 | THREAD_INSTRUCTION_COUNT_OVERFLOW | 无(仅 10^9 卡死保护) |
| 非法 opcode | Quitf("Invalid opcode...") src/dev_ng/rage/script/src/script/thread.cpp:1913-1917 | FaultQuit src/dev_ng/rage/script/src/script/thread.cpp:5298 |
| 调用缺失 native | InvalidNative → Quitf src/dev_ng/rage/script/src/script/thread.cpp:2155-2161 | 同左 |
两种 Fault 的后续处理不同:
- 成员函数
scrThread::Fault(pc, msg, type)(调试核心与Reset使用)回滚 PC 到出错指令、记录m_AbortReason、非 FINAL 打印栈回溯,调用调试器回调和应用注册的AppFaultHandler,最后置 ABORTEDsrc/dev_ng/rage/script/src/script/thread.cpp:502-524。GTAV 的AppFaultHandler = CTheScripts::GtaFaultHandler会向脚本错误事件组投递CEventErrorArrayOverflow/StackOverflow/InstructionLimit/UnknownErrorsrc/dev_ng/game/script/script.cpp:420-456。 - 快速核心的
Fault(...)宏记录 PC、diagLoggedPrintf、调用FaultCommon,然后返回 ABORTEDsrc/dev_ng/rage/script/src/script/thread.cpp:4616-4617;FaultCommon在非 FINAL 下打印最近调用栈并Quitf("Script fault (usually an array overrun...)"),FINAL 下什么也不做src/dev_ng/rage/script/src/script/thread.cpp:4700-4710。由此可见(基于代码观察):在使用快速核心的构建中,VM fault 不会调用AppFaultHandler,CEventError*事件只会在调试核心路径下产生;FINAL 中出错的线程只是静默进入 ABORTED,随后被 GTAV 的回收逻辑清理。
9.2 CATCH/THROW#
CATCH 把当前 PC/FP/SP 存入 m_CatchPC/FP/SP 并压入 -1;THROW 取栈顶值,若无 CATCH 则 fault,否则恢复快照并把抛出值作为 CATCH 的“返回值” src/dev_ng/rage/script/src/script/thread.cpp:5232-5254。只有一层,不能嵌套(script.dtx 明示其用途是“重启”类流程) src/dev_ng/rage/script/src/script/script.dtx:274-287。C++ 可用 scrThread::Throw(value) 从外部触发(不能对当前正在执行的线程使用),它同时解除阻塞 src/dev_ng/rage/script/src/script/thread.cpp:1957-1970。GTAV 的 ForceCleanup 机制用类似思路但独立实现(见 script-handlers-and-threads.md)。
10. native 注册表与内置命令#
- 注册表
s_CommandHash是scrCommandHash<scrCmd>:256 个顶层桶,每个桶节点存 7 项并链式扩展;Insert断言重复哈希src/dev_ng/rage/script/src/script/thread.cpp:137、src/dev_ng/rage/script/src/script/thread.h:861-972。其中链指针、计数和哈希以sysObfuscated包装存储(防篡改措施,仅记录存在)。 RegisterCommand(u64 hash, handler [, name, file, sig]):插入s_CommandHash;SCRIPT_DEBUGGING下另外登记“名字 → 处理函数”、native 追踪信息,以及按scrComputeHash(name)索引的s_SignatureHash(签名 + 函数)src/dev_ng/rage/script/src/script/thread.cpp:2607-2631、src/dev_ng/rage/script/src/script/thread.cpp:148。RegisterUnusedCommand仅非 FINAL 存在,可用-testunusednatives跳过注册以模拟发行构建src/dev_ng/rage/script/src/script/thread.cpp:2573-2604。scrThread::InitClass初始化命令表、挂接sysStack::PrePrintStackTrace以便 C++ 崩溃栈里附带脚本栈、注册 Rocky 符号解析回调src/dev_ng/rage/script/src/script/thread.cpp:2525-2554。ExecuteNetworkCommand(hash, n, params)按哈希直接调用 native,但它查的是只在SCRIPT_DEBUGGING下填充的s_SignatureHashsrc/dev_ng/rage/script/src/script/thread.cpp:2789-2799;游戏代码未调用它(在game/中检索无结果)。
RegisterBuiltinCommands 注册 VM 自带命令 src/dev_ng/rage/script/src/script/thread.cpp:4412-4529:
| 类别 | 命令 | 注册方式 |
|---|---|---|
| 阻塞/线程 | WAIT、START_NEW_SCRIPT[_WITH_ARGS][_WITH_NAME_HASH[_AND_ARGS]]、SET_THIS_THREAD_PRIORITY | 直接 RegisterCommand(REGISTER_BUILTIN_COMMAND) / SCR_REGISTER_SECURE |
输出(!__NO_OUTPUT) | PRINTLN[_FINAL]、ASSERTLN、CLOGLN、CPRINTLN、CWARNINGLN、CERRORLN、CASSERTLN、CDEBUG1/2/3LN、频道注册/级别 | 内置 / UNUSED |
| 计时 | TIMERA/B、SETTIMERA/B、TIMESTEP | SECURE |
| 数学 | SIN COS SQRT POW LOG10 VMAG VMAG2 VDIST VDIST2 SHIFT_LEFT SHIFT_RIGHT FLOOR CEIL ROUND TO_FLOAT | SECURE |
| 调试 | PRINTSTRING/FLOAT/INT/VECTOR/NL、DEBUG_PRINTCALLSTACK、BREAKPOINT、控制台命令读取、ROCKY_TAG(仅 RSG_PROSPERO || RSG_SCARLETT) | UNUSED(FINAL 中不注册) |
非 FINAL 还向 bank 控制台添加 script、set_global_var/sgv、get_global_var/ggv、call(直接调用 native)、regs,调试构建再加 scrbreak、scrtrace 与 “Script Native Trace” bank src/dev_ng/rage/script/src/script/thread.cpp:4499-4528。REGISTER_BUILTIN_COMMAND 在调试构建传入名字与空签名,否则只传哈希 src/dev_ng/rage/script/src/script/thread.cpp:4404-4408。
11. 调试与剖析#
- 断点/单步(
SCRIPT_DEBUGGING):线程级m_threadBreakpoints与程序级m_programBreakpoints;命中时置 HALTED 并回调m_debugBreakpointCallback;单步通过SetStepRegion(minPc,maxPc)在 PC 离开半开区间时停下src/dev_ng/rage/script/src/script/thread.cpp:1013-1034、src/dev_ng/rage/script/src/script/program.cpp:1092-1130、src/dev_ng/rage/script/src/script/thread.h:485-488。 - scriptgui /
scrDebugger:RAG widget 形式的调试器,按线程解析 static/local/global 与调用栈并构建控件,可由CreateDebugger/RegisterDebugger托管并在UpdateAll中刷新src/dev_ng/rage/script/src/scriptgui/debugger.h:30-114。它读取编译器生成的.scd调试信息([VERSION]/[VARIABLES]/[LINEINFO]/[EOF]分节,版本不符则Quitf要求重编)src/dev_ng/rage/script/src/scriptgui/debugger.cpp:720-790、src/dev_ng/rage/script/src/scriptgui/debugger.cpp:927、src/dev_ng/rage/script/src/scriptgui/debugger.cpp:957;.scd的写出见src/dev_ng/rage/script/tools/scriptcompiler/sc.cpp:378-411。“Load Script” 按钮会同时加载.sco对应的全局符号src/dev_ng/rage/script/src/scriptgui/debugger.cpp:2176-2185。scrWidgetEditor是嵌入 RAG 的脚本编辑控件src/dev_ng/rage/script/src/scriptgui/widgeteditor.h:21。GTAV 在 BANK 下为指定脚本创建scrDebugger(GtaThread::pDebugger)。 - 剖析:
SCRIPT_PROFILING下-scriptprofiling[=N]打开逐子程序的包含/独占/native 时间与指令统计,默认每 250 次更新清零src/dev_ng/rage/script/src/script/thread.cpp:687-690、src/dev_ng/rage/script/src/script/thread.h:603-633;USE_PROFILER下两套核心都在ENTER/LEAVE推入/弹出 Rocky 事件,并支持自动采样调用栈src/dev_ng/rage/script/src/script/thread.cpp:4902-4907、src/dev_ng/rage/script/src/script/thread.cpp:3050-3070。 - 数组尺寸监视(非 FINAL):
CScriptArraySizeMonitor记录某地址初值,并在每条指令/每次 native 调用后比对,用于抓“谁改了数组长度”src/dev_ng/rage/script/src/script/thread.h:117-134、src/dev_ng/rage/script/src/script/thread.cpp:1929-1931。
12. scriptgui 之外的辅助库#
bankscript.cpp:rageBankScriptBindings::Register_BankScript()注册按路径读写 RAG widget 的 native(WIDGET_EXISTS、GET/SET_WIDGET_VALUE_*等),函数体在非 BANK 构建中为空实现src/dev_ng/rage/script/src/script/bankscript.cpp:779-806。scr_grcore:rageScriptBindings::Register_grcore(grcSetup&)注册GRC_BEGIN_UPDATE/…/GRC_COLOR4F等即时模式绘制命令,服务于rage/script/samples/sample_scr_grcoresrc/dev_ng/rage/script/src/scr_grcore/grcore.cpp:105-123。GTAV 的game/script中未检索到对它的引用,属于 RAGE 示例层。scr_scaleform:Scaleform 脚本绑定,见 ../18-frontend-ui/scaleform-integration.md。
13. native 哈希混淆与相关防篡改(仅流程层面)#
以下机制存在于脚本运行时边界上,本 Wiki 只记录位置与流程,不展开映射、密钥或检测细节(总体架构见 ../23-debug-security/security-overview.md):
- native 标识随机化:
.sch中每个NATIVE声明都必须带一个 64 位随机值,C++ 注册宏使用同一值;randomizenatives工具批量生成/同步这些值(见 script-compiler-and-toolchain.md)。register_secure.cpp/update_secure.cpp是早期把SCR_REGISTER(name, fn)改写为带名字哈希的SCR_REGISTER_SECURE的文本替换工具src/dev_ng/rage/script/src/script/register_secure.cpp:40-70、src/dev_ng/rage/script/src/script/update_secure.cpp:40-90。 - 程序内 native 表的变换:
Save打乱表顺序;资源化时DeclareStruct对每项施加可逆变换,scrProgram(datResource&)逆变换(3.3/3.4 节)。 - 注册表与注册顺序:命令表项以
sysObfuscated存储;GTAV 启动时以随机顺序调用各SetupScriptCommands(CTheScripts::ScrambleAndRegister,见 natives-and-commands.md)。 - PC 专属:
scrThread::sm_ProgIdRunFunctions记录每个程序对应的线程Run虚函数地址用于运行期抽查src/dev_ng/rage/script/src/script/thread.h:153-161、src/dev_ng/rage/script/src/script/thread.cpp:3154-3182;SCR_REGISTER_SECURE_HONEYPOT宏与ApiCheckPlugin_*钩子把 native 注册与game/security插件相连;GTAV 的CScriptGlobalTamperChecker在CTheScripts::Process外围检查全局内存(见 script-handlers-and-threads.md)。
14. 遗留、独立与未启用的代码#
| 文件 | 状态 |
|---|---|
threadrun.cpp、threadrun2.cpp | 不含 RAGE 头文件的独立解释器版本(自带简化的 scrValue/scrProgram/Info 定义),通过 build_driver.bat/build_threadrun_lib.bat 用 PS3 ps3ppusnc 单独编译成 libthreadrun.a src/dev_ng/rage/script/src/script/threadrun.cpp:1-60、src/dev_ng/rage/script/src/script/build_threadrun_lib.bat:1-9。不在当前工程中 |
threadrunasm.s | SN Systems PS3 PPU 编译器为 threadrun.cpp 生成的汇编输出(PPC 指令) src/dev_ng/rage/script/src/script/threadrunasm.s:1-40 |
threadtest1-6.cpp | 早期解释器测试片段 |
scriptrun.cpp | 命令行运行器:可加载资源化脚本,InitClass/RegisterBuiltinCommands/AllocateThreads(20) 后 UpdateAll(200000000) 计时 src/dev_ng/rage/script/src/script/scriptrun.cpp:11-39(调用的 InitClass(953) 签名与当前无参 InitClass() 不符,说明已不可编译——推断) |
src/script/scannative.cpp | 旧版 scannative(用法提示提到 SCR_DEV_ONLY);现行版本在 tools/scannative |
csupport.h、csupportentry.h | 脚本“编译为 C++/PRX”(EXECUTABLE_SCRIPTS)的运行时支持;该开关被硬编码为 0 src/dev_ng/rage/script/src/script/program.h:18,Exec/COMPILED_SCRIPT_YIELD 路径不编译 src/dev_ng/rage/script/src/script/thread.cpp:710-807 |
SCRVALUE_OFFSETS 相关 EncodePointer/DecodePointer | 把 64 位指针压成 32 位偏移的实验,已禁用 src/dev_ng/rage/script/src/script/thread.cpp:2493-2523 |
15. 约束与边界行为(速查)#
- 同时驻留程序 ≤ 175;每程序 native ≤ 4096(编译器
scrNode::MaxNativessrc/dev_ng/rage/script/tools/scriptcompiler/node.h:186);native 参数槽 ≤ 63、返回槽 ≤ 3;子程序参数 < 256、帧大小 < 65536src/dev_ng/rage/script/tools/scriptcompiler/node.cpp:1506-1520。 - 全局块 64 个,每块 ≤ 2^18 槽;签名不一致即
Quitf。所有程序都卸载时全局状态整体重置。 - 线程调用栈仅记录最近 16 层(
MAX_CALLSTACK)src/dev_ng/rage/script/src/script/thread.h:113;Info一次 native 调用最多缓冲 4 个Vector3&参数(MAX_VECTOR3)src/dev_ng/rage/script/src/script/thread.h:238-247。 - 脚本运行完全在调用
UpdateAll的线程(GTAV 为主更新线程)上执行;VM 本身没有锁,唯一的临界区在程序注册表(流式加载可能在别的线程构造程序)。 - 一个线程自杀(
Kill自身或主函数返回)后保持 ABORTED,资源(栈、程序引用)要等下一次外部Kill才释放。
16. 未决问题 / 证据不足#
ENTER的 u16 操作数在编译器中名为VarCount,其精确含义(是否包含参数与两个链接槽)需要结合scr.y的BeginStackFrame/EndStackFrame进一步核对;本页只描述了解释器对它的使用方式。- 快速核心在 FINAL 中出错静默中止、不通知
AppFaultHandler:这是从代码路径推出的结论,未在运行中验证;也可能有其他(游戏侧)机制监测 ABORTED 线程。 fwAssetRscStore构造参数1170的确切含义(默认槽位数)未追到fwAssetRscStore实现。