Udon Decompiler

Udon VM

Udon VM 是一个简单的栈式虚拟机.

堆, 栈和寄存器

  • 堆: 是一个 IStrongBox[]IStrongBox[], 地址就是数组索引, 使用程序中的常量段初始化
  • 栈: 一个 u32u32
  • PC 寄存器: 单位是字节

外部函数

Udon VM 的外部函数委托是 UdonExternDelegateUdonExternDelegate, 具体定义为

delegate void UdonExternDelegate(IUdonHeap heap, Span<uint> parameterAddresses);
delegate void UdonExternDelegate(IUdonHeap heap, Span<uint> parameterAddresses);

也即传入

  • 堆用于获取参数和写入结果
  • 一系列参数地址(在堆中的)用于获取参数

在此基础上封装了 CachedUdonExternDelegateCachedUdonExternDelegate, 具体定义为

class CachedUdonExternDelegate
{
public readonly string externSignature;
public readonly UdonExternDelegate externDelegate;
public readonly int parameterCount;
}
class CachedUdonExternDelegate
{
public readonly string externSignature;
public readonly UdonExternDelegate externDelegate;
public readonly int parameterCount;
}

CachedUdonExternDelegateCachedUdonExternDelegate 可以完全通过一个 stringstring 获取, 也即 externSignatureexternSignature.

这个 externSignatureexternSignature 是 Udon Node 的方法签名, 相关生成代码在 UdonSharp.​Compiler.​Udon.​CompilerUdonInterfaceUdonSharp.​Compiler.​Udon.​CompilerUdonInterface 中, 一些签名的例子如

UnityEngineGameObject.__SetActive__SystemBoolean__SystemVoid
VRCDynamicsVRCConstraintSource.__set_ParentPositionOffset__UnityEngineVector3
ExternVRCEconomyIProduct.__get_Name__SystemString
UnityEngineColor.__op_Addition__UnityEngineColor_UnityEngineColor__UnityEngineColor
UnityEngineGameObject.__SetActive__SystemBoolean__SystemVoid
VRCDynamicsVRCConstraintSource.__set_ParentPositionOffset__​UnityEngineVector3
ExternVRCEconomyIProduct.__get_Name__SystemString
UnityEngineColor.__op_Addition__UnityEngineColor_UnityEngineColor__​UnityEngineColor

这些名字由两部分组成, 分别是 ModuleNameModuleNameFuncSignatureFuncSignature. 类(也即 ModuleModule)通过实现 IUdonWrapperModuleIUdonWrapperModule, 将自己的 ModuleNameModuleName 和所有 FuncSignatureFuncSignature 及其对应的参数数量注册到 UdonWrapperUdonWrapper 中, 供其使用完整的 externSignatureexternSignature 获取.

内部函数

除了入口点表一节中提到的入口点表外, Udon Sharp 在生成函数时候还做了其他的处理.

UdonSharp 编译器所产生的大多数(包括非公开的)函数有两个入口: 公开入口和内部入口. 公开入口用于外部调用, 从公开入口进入函数, 其执行结果是 Udon VM 停机. 从内部入口进入函数, 其执行结果是跳回到某个调用函数的 JUMPJUMP 之后或停机. 在汇编层面, 两者的区别是公开入口比内部入口多了一句 PUSH __const_SystemUInt32_0PUSH __const_SystemUInt32_0, 这里 __const_SystemUInt32_0__const_SystemUInt32_0 的地址不固定, 但是其值永远是 42949672954294967295, 也即 0xFFFFFFFF0xFFFFFFFF. 当这个值被写入 PC(也即 JUMPJUMP 到这个地址) 时, Udon VM 会停机.

一些通过其他方式产生过的 Udon 程序的函数则不一定有两种入口, 参见 paran3xus/udon-decompiler#12.

函数的返回被编译为

PUSH, __intnl_returnJump_SystemUInt32_0
COPY
JUMP_INDIRECT, __intnl_returnJump_SystemUInt32_0
PUSH, __intnl_returnJump_SystemUInt32_0
COPY
JUMP_INDIRECT, __intnl_returnJump_SystemUInt32_0

此处 __intnl_returnJump_SystemUInt32_0__intnl_returnJump_SystemUInt32_0 的名字固定, 且堆地址总是是 22. 当通过公开入口进入时, 这里的 __intnl_returnJump_SystemUInt32_0__intnl_returnJump_SystemUInt32_0 也就被写入了 0xFFFFFFFF0xFFFFFFFF, 最终使 Udon VM 停机. 而当内部入口进入时, 一种可能是由调用者负责在调用前在堆中压入返回地址(也即 JUMPJUMP 指令的下一条指令的地址), 另一种可能是不压入返回地址, 则这个函数执行结束后会跳回到某个调用函数的 JUMPJUMP 之后, 甚至停机.

为什么是某个调用函数呢, 考虑如果某个函数的最后一条指令就是调用另一个内部函数, 且在 JUMPJUMP 前没有压入返回地址, 那么被调用函数就会代该函数返回, 如果该函数是通过公开入口进入的, 则会代其令 Udon VM 停机.

还有一部分函数没有公开入口.

执行过程

不断读取当前 PC 处的指令并执行, 直到停机或 PC 超出当前程序有效指令空间或 PC 为 0xFFFFFFFF0xFFFFFFFF. 不同指令的执行策略为

  • NOPNOP: PC 步进 4 字节
  • ANNOTATIONANNOTATION: PC 步进 8 字节
  • PUSHPUSH: 把 OPERANDOPERAND 作为立即数压栈, PC 步进 8 字节
  • POPPOP: 弹栈, 丢弃栈顶值, PC 步进 4 字节
  • JUMPJUMP: 设置 PC 为立即数 OPERANDOPERAND
  • JUMP_IF_FALSEJUMP_IF_FALSE: 栈顶是堆地址, 弹栈, 读该地址对应的堆元素(boolbool)的值

    • 若为 truetrue, PC 步进 8 字节
    • 若为 falsefalse, 设置 PC 为立即数 OPERANDOPERAND
  • JUMP_INDIRECTJUMP_INDIRECT: 设置 PC 为 OPERANDOPERAND 作为堆地址指向的 u32u32
  • EXTERNEXTERN: 调用外部函数. 尝试读取 OPERANDOPERAND 作为堆地址指向的对象

    • 若为 stringstring, 通过 UdonWrapperUdonWrapper 获取该 stringstring 对应的 CachedUdonExternDelegateCachedUdonExternDelegate (这是通常的情况)
    • 若为 CachedUdonExternDelegateCachedUdonExternDelegate, 也得到了 CachedUdonExternDelegateCachedUdonExternDelegate

    从栈中连续弹出 CachedUdonExternDelegate.parameterCountCachedUdonExternDelegate.parameterCount 个参数地址, 按与弹栈相反的顺序(也即最初的栈顶为最后一个地址)组装成 Span<uint> parameterAddressesSpan<uint> parameterAddresses, 并调用 UdonExternDelegateUdonExternDelegate. PC 步进 8 字节

    在调用者(对于非静态函数)或返回值存在的情况下, 调用者和返回值分别作为第一个和最后一个参数传入.

  • COPYCOPY: 从栈中先后弹出 TARGETTARGETSOURCESOURCE 两个地址, 然后把堆中 TARGETTARGET 地址指向的值使用 SOURCESOURCE 地址指向的值覆盖. 所在 PC 步进 4 字节

从这里也可以看出, 栈中的值都是堆地址. 也即除了 JUMPJUMPJUMP_IF_FALSEJUMP_IF_FALSE 之外的所有指令的 OPERANDOPERAND 值都是堆地址.