Udon Program
Udon Program 是 Udon Script 的编译产物, 每个 Udon Program 只代表一个类.
使用 AssetRipper 解包地图后, 能得到大量 AssetRipper 无法正确解析的 MonoBehaviour 资产文件. 其中一些 MonoBehaviour 资产包含一个很长的 serializedProgramCompressedBytesserializedProgramCompressedBytes. 这代表这个资产是一个 Udon Script 的编译产物.
serializedProgramCompressedBytesserializedProgramCompressedBytes 是一个十六进制字符串, 是 GZip 压缩后的 Udon Program 序列化结果.
serializedProgramCompressedBytesserializedProgramCompressedBytes 经过 GZip 解压后得到的二进制文件是 UdonProgramUdonProgram 实例序列化后的结果.
这个序列化过程使用的是一个 VRChat 修改的 OdinSerializerOdinSerializer. 所以我们可以直接用这个序列化器对应的反序列化器进行反序列化. 一些关键代码如下
using System.IO;using VRC.Udon.Common;using VRC.Udon.Serialization.OdinSerializer;using var memoryStream = new MemoryStream(fileData);var context = new DeserializationContext();var reader = new BinaryDataReader(memoryStream, context);UdonProgram program = VRC.Udon.Serialization.OdinSerializer.SerializationUtility .DeserializeValue<UdonProgram>(reader);
using System.IO;using VRC.Udon.Common;using VRC.Udon.Serialization.OdinSerializer;using var memoryStream = new MemoryStream(fileData);var context = new DeserializationContext();var reader = new BinaryDataReader(memoryStream, context);UdonProgram program = VRC.Udon.Serialization.OdinSerializer.SerializationUtility .DeserializeValue<UdonProgram>(reader);
UdonProgramUdonProgram 类中几乎有我们需要的一切. 下面是一个简化的类定义
public class UdonProgram : IUdonProgram{ public string InstructionSetIdentifier { get; } public int InstructionSetVersion { get; } public byte[] ByteCode { get; } public IUdonHeap Heap { get; } public IUdonSymbolTable EntryPoints { get; } public IUdonSymbolTable SymbolTable { get; } public IUdonSyncMetadataTable SyncMetadataTable { get; } public int UpdateOrder { get; }}
public class UdonProgram : IUdonProgram{ public string InstructionSetIdentifier { get; } public int InstructionSetVersion { get; } public byte[] ByteCode { get; } public IUdonHeap Heap { get; } public IUdonSymbolTable EntryPoints { get; } public IUdonSymbolTable SymbolTable { get; } public IUdonSyncMetadataTable SyncMetadataTable { get; } public int UpdateOrder { get; }}
我们比较关心 ByteCodeByteCode, HeapHeap, EntryPointsEntryPoints, SymbolTableSymbolTable 这几个字段.
是一系列大端序 u32u32 组成的指令的序列.
指令格式为 OPCODE[OPERAND]OPCODE[OPERAND], 两部分各 4 字节, OPERANDOPERAND 是一个大端序 u32u32.
OPCODEOPCODE 包括无参数的 NOPNOP, POPPOP, COPYCOPY 和有一个参数的 PUSHPUSH, JUMP_IF_FALSEJUMP_IF_FALSE, JUMPJUMP, EXTERNEXTERN, ANNOTATIONANNOTATION, JUMP_INDIRECTJUMP_INDIRECT.
各 OPCODEOPCODE 对应的值为
class OpCode(IntEnum): NOP = 0 PUSH = 1 POP = 2 JUMP_IF_FALSE = 4 JUMP = 5 EXTERN = 6 ANNOTATION = 7 JUMP_INDIRECT = 8 COPY = 9
class OpCode(IntEnum): NOP = 0 PUSH = 1 POP = 2 JUMP_IF_FALSE = 4 JUMP = 5 EXTERN = 6 ANNOTATION = 7 JUMP_INDIRECT = 8 COPY = 9
各 OPCODEOPCODE 和 OPERANDOPERAND 含义如下:
NOPNOP: 空指令PUSH IPUSH I: 将立即数II压栈POPPOP: 从栈中弹出一个值并丢弃COPYCOPY: 复制堆中的值JUMP_IF_FALSE ADDRJUMP_IF_FALSE ADDR: 条件跳转到ADDRADDRJUMP ADDRJUMP ADDR: 无条件跳转到ADDRADDREXTERN FEXTERN F: 调用外部函数,FF是堆中的函数签名stringstring或者函数委托UdonExternDelegateUdonExternDelegate的地址ANNOTATIONANNOTATION: 注解, 执行时跳过JUMP_INDIRECT IADDRJUMP_INDIRECT IADDR: 间接跳转到IADDRIADDR作为堆地址指向的值
用于存储 Udon VM 执行该 Udon Program 时堆的初始值, 相当于常量段.
简化的类定义如下
[Serializable]public sealed class UdonHeap : IUdonHeap, ISerializable{ [NonSerialized] private readonly IStrongBox[] _heap; public void GetObjectData( SerializationInfo info, StreamingContext context ) { List<ValueTuple<uint, IStrongBox, Type>> list = new List<ValueTuple<uint, IStrongBox, Type>>(); this.DumpHeapObjects(list); info.AddValue("HeapCapacity", Math.Max(0, this._heap.Length)); info.AddValue("HeapDump", list); } public void DumpHeapObjects( List<ValueTuple<uint, IStrongBox, Type>> destination ) { uint num = 0; while (num < this._heap.Length) { IStrongBox strongBox = this._heap[num]; if (strongBox != null) { destination.Add(new ValueTuple<uint, IStrongBox, Type>( num, strongBox, strongBox.GetType().GenericTypeArguments[0] )); } num += 1; } }}
[Serializable]public sealed class UdonHeap : IUdonHeap, ISerializable{ [NonSerialized] private readonly IStrongBox[] _heap; public void GetObjectData( SerializationInfo info, StreamingContext context ) { List<ValueTuple<uint, IStrongBox, Type>> list = new List<ValueTuple<uint, IStrongBox, Type>>(); this.DumpHeapObjects(list); info.AddValue("HeapCapacity", Math.Max(0, this._heap.Length)); info.AddValue("HeapDump", list); } public void DumpHeapObjects( List<ValueTuple<uint, IStrongBox, Type>> destination ) { uint num = 0; while (num < this._heap.Length) { IStrongBox strongBox = this._heap[num]; if (strongBox != null) { destination.Add(new ValueTuple<uint, IStrongBox, Type>( num, strongBox, strongBox.GetType().GenericTypeArguments[0] )); } num += 1; } }}
我们感兴趣的就是其中的 HeapDumpHeapDump, 这是一个 (Addr, Value, Type)(Addr, Value, Type) 三元组的列表.
实际上是公开函数表.
简化的类定义如下
[Serializable]public sealed class UdonSymbolTable : IUdonSymbolTable, ISerializable{ private readonly ImmutableArray<string> _exportedSymbols; private readonly ImmutableDictionary<string, IUdonSymbol> _nameToSymbol; void ISerializable.GetObjectData( SerializationInfo info, StreamingContext context ) { info.AddValue( "Symbols", this._nameToSymbol.Values.ToList<IUdonSymbol>() ); info.AddValue( "ExportedSymbols", this._exportedSymbols.ToList<string>() ); }}[Serializable]public sealed class UdonSymbol : IUdonSymbol, ISerializable{ public string Name { get; } public Type Type { get; } public uint Address { get; } void ISerializable.GetObjectData( SerializationInfo info, StreamingContext context ) { info.AddValue("Name", this.Name); info.AddValue("Type", this.Type); info.AddValue("Address", this.Address); }}
[Serializable]public sealed class UdonSymbolTable : IUdonSymbolTable, ISerializable{ private readonly ImmutableArray<string> _exportedSymbols; private readonly ImmutableDictionary<string, IUdonSymbol> _nameToSymbol; void ISerializable.GetObjectData( SerializationInfo info, StreamingContext context ) { info.AddValue( "Symbols", this._nameToSymbol.Values.ToList<IUdonSymbol>() ); info.AddValue( "ExportedSymbols", this._exportedSymbols.ToList<string>() ); }}[Serializable]public sealed class UdonSymbol : IUdonSymbol, ISerializable{ public string Name { get; } public Type Type { get; } public uint Address { get; } void ISerializable.GetObjectData( SerializationInfo info, StreamingContext context ) { info.AddValue("Name", this.Name); info.AddValue("Type", this.Type); info.AddValue("Address", this.Address); }}
这里每个 UdonSymbolUdonSymbol 里的
NameName是函数名AddressAddress是该函数的首条指令在UdonProgram.ByteCodeUdonProgram.ByteCode中的索引TypeType对于入口点来说, 无意义
这给我们带来了很多方便.
类定义和入口点表相同, 其中每个 UdonSymbolUdonSymbol 里的
NameName是符号名AddressAddress是该符号在堆中的地址TypeType是符号类型