CD Code Digest Hub /GTA V

虚拟机、调用栈、错误与安全边界

6 分钟阅读 9 个章节

1. 程序对象与执行实例#

scrProgram 保存可共享的代码、字符串、Native 表、静态初值和布局信息;scrThread 保存某一次执行的 PC、SP、FP、计时器、等待状态和栈。GTA 的 GtaThread 继承该模型,增加任务/网络/清理等游戏语义。[S1][S2]

text
scrProgram(共享)
 ├─ 字节码页 / 字符串页
 ├─ Native 表:装载后解析为包装器地址
 └─ 静态初值 / 参数大小 / 全局块信息

scrThread A                       scrThread B
 ├─ PC / FP / SP / WAIT 状态        ├─ 独立执行状态
 └─ Stack                         └─ 独立 Stack
     静态区 + 启动参数
     调用帧 + 局部变量 + 求值栈

GLOBALS:由全局块管理,共享于相关实例

因此运行两个相同脚本不必复制字节码,但会占用各自实例状态和预分配栈。普通实例静态变量位于同一栈分配的底部,不应与共享 GLOBALS 混淆。

2. Reset 与启动#

Reset 确认程序有效,在提供非零实参大小时检查它与声明匹配,复制静态初值,把提供的启动参数写入静态区尾部的隐藏位置,设置 SP/FP、计时器和 CATCH 状态,并压入入口返回地址。静态区大到占满栈时直接报错。[S2]

这表明“栈大小”不仅承载函数局部和表达式临时量,也覆盖大量脚本实例变量。仅按照最大调用深度估算栈会低估资源需求。

3. 调用约定#

CALL 保存返回 PC 并转到目标;ENTER 读取参数槽数和局部数,保存旧 FP、建立新帧并初始化局部;LEAVE 恢复 FP/PC,丢弃参数和局部,把返回槽移回调用者栈顶。[S3]

返回地址为零表示入口结束,VM 使用 ABORTED 状态表示执行终止,所以 ABORTED 不能只解释为异常崩溃:正常脚本结束也会沿该状态结束生命周期。实际故障需要结合 Fault 信息与游戏层清理判断。

CALLINDIRECT 从栈上取得函数地址,开发路径可捕获未初始化函数指针。函数地址属于当前程序的字节码空间,并没有常规动态闭包对象。

4. 两套解释执行路径#

thread.cpp 同时包含便于调试/剖析的解释路径和末尾合入的快速内核。SMALL_SCRIPT_CORE 根据 SCRIPT_DEBUGGING / SCRIPT_PROFILING 选择;有些开发场景也可通过 fastscriptcore 进入快速内核。[S4]

快速内核把 SP/FP/PC 缓存在局部变量,以紧凑 dispatch 循环执行。THREADED_INTERPRETER 在当前合入实现中受 RSG_ORBIS 控制;使用 computed goto 的“threaded interpreter”术语是指指令派发技术,不是每条脚本运行在一个操作系统线程上。

两个路径不是完全相同的观察环境:断点、调用记录、计数保护、引用检查和部分错误诊断随配置变化。做性能比较时应记录实际内核、构建宏和剖析开关,不能直接以调试版运行时间代表发布成本。

5. NATIVE 是 VM 与游戏的控制边界#

装载 scrThread::Validate 将 Native ID 解析为 C++ 包装器地址。NATIVE 取表项、保存执行状态,创建 Info(ResultPtr, ParamCount, Params),调用包装器;若脚本状态仍 RUNNING,复制向量引用返回值并调整栈。[S5]

若 Native 把状态改成 BLOCKED,VM 保留该调用的 PC 和参数,下次重新进入同一 Native,由它判断异步操作是否完成。这个设计用一个很小的协议实现跨帧顺序代码,详细时序见调度并发与生命周期。

6. 异常、故障与恢复#

机制执行含义不应推断的能力
CATCH/THROW单个保存点,恢复 PC/FP/SP 并传整数结果嵌套 typed exception、finally、析构展开
Fault记录故障、通知回调并设置 ABORTED所有错误都能被脚本捕获
HALTED调试暂停游戏所有系统必然同步暂停
Gta force cleanup游戏条件触发的恢复与退出流程与 CATCH 完全同一机制

数组越界、栈溢出、缺少 Native、错误 PC 等有不同路径的检测。无效 opcode 在某些路径可能走 Quitf,而非单纯结束一个脚本。新语言若用于 UGC/不可信内容,应明确定义“单实例失败”和“宿主不可继续”的故障等级。[S3–S5]

7. 语义一致性问题#

当前解释路径对整数零除数保留左操作数,而编译期折叠返回零;快速浮点除法还存在用 Int 位模式检查分母是否零的写法,与调试路径的 float 检查不完全相同。[S6] 例如正零与负零的位模式不同,值得纳入差分测试。

这些是静态审查中发现的语义风险,未实际编译或执行验证。不能据此给出零售游戏故障结论,也不应把它们整理成推荐语言规范。正确借鉴是:用统一语义和跨内核测试减少“开发版能解释、发布版另有行为”。

8. 这不是经证明的安全沙箱#

脚本没有通用裸指针语法,不代表所有内存都不可越界。VM 使用原生指针/引用、信任程序布局和 Native 写入;部分检查仅在 DEV/BANK 打开。Validate 主要做 Native 解析及相关调试准备,不是能证明任意恶意字节码栈平衡、跳转合法、类型正确的完整验证器。[S5]

对内部受信内容,这是可以理解的效率权衡;对新 UGC 语言,需要在装载时验证指令边界、控制流、栈高度、索引和调用签名,运行时保留预算与内存边界检查,并以权限化 Native 限制宿主能力。加密资源与 ID 隐藏不能替代这些机制。

源码证据#

  • S1 — scrProgram;scrThread 与 Serialized。
  • S2 — Reset。
  • S3 — CALL/ENTER/LEAVE;CATCH/THROW。
  • S4 — Run 路径选择;快速内核。
  • S5 — Validate;Native 快速调用。
  • S6 — 常量求值;普通算术执行;快速算术。