これは?
VRChatでUdonというVMがつかえるようになったらしいのでアセンブリを分析して読んでみた。読めるとこだけ。
なお、調査には cannorin 氏のtweet Udon VM 向けのコンパイラを作ろうとしている[1] を参考にした。
あと当方Unityと.NETとC#に明るくないのでデータ構造以外の細かい認識なんかは保証しない。
概要
Udon VMは.NET VM上で動くVM(多分厳密にはUnityのMonoらしい)。
公式情報は以下にある
https://ask.vrchat.com/t/getting-started-with-udon/80
https://ask.vrchat.com/t/getting-started-with-udon-assembly/84
この公式サイトによると存在するニーモニックは以下の通り。
- NOP
- PUSH, Adress
- POP
- JUMP_IF_FALSE, Address
- JUMP, Address
- EXTERN, ExternMethodSignature
- ANNOTATION
- JUMP_INDIRECT, Address
- COPY
計算ができないのでどうするのかとおもったら、EXTERNで外部に投げつけて処理するっぽい。
IUdonProgram
細かい話はさておき、このVMで動くデータはIUdonProgramというIFで表現される
namespace VRC.Udon.Common.Interfaces
{
public interface IUdonProgram
{
string InstructionSetIdentifier { get; }
int InstructionSetVersion { get; }
byte[] ByteCode { get; }
IUdonHeap Heap { get; }
IUdonSymbolTable EntryPoints { get; }
IUdonSymbolTable SymbolTable { get; }
IUdonSyncMetadataTable SyncMetadataTable { get; }
}
}
コード部分がByteCodeで他データがいろいろなクラスに入っていると思われる。
ByteCode
ザックリ調べてみるとオペコードは1 byte, オペランドは0-4 bytesの可変長なバイトコードを吐く様子。
オペランド | オペコード | 説明 |
---|---|---|
0x00 | none | NOP: なにもしない |
0x01 | Address(4bytes) | PUSH: アドレスをstackにpushする |
0x02 | none | POP: stackからアドレスをpopする(そのまま捨てられそう) |
0x03 | 不明 | |
0x04 | Address(4bytes) | JUMP_IF_FALSE: 最新のstackをpopし、それがtrueならPCにAddressをセットする |
0x05 | Address(4bytes) | JUMP: PCにAddressをセットする |
0x06 | ExternMethodSignature(4bytes) | EXTERN: |
0x07 | ???(4bytes) | ANNOTATION: なにもしない、らしいがドキュメントと違って値を求められた。ANNOTATION自体が予約状態かもしれないから使うべきではなさそう |
0x08 | Address(4bytes) | JUMP_INDIRECT: PCにHeapのAddressに入っている値をセットする |
0x09 | none | COPY: |
AddressはHeapのアドレスの様子。
ExternMethodSignatureもHeapのアドレスだった。
逆アセンブルとバイトコードの対応例
前述の https://ask.vrchat.com/t/getting-started-with-udon-assembly/84 のコードを使って、その逆アセンブルとバイトコードの対応例を示す。
0x0100000003 PUSH, 0x00000003
0x0100000002 PUSH, 0x00000002
0x09 COPY
0x0100000004 PUSH, 0x00000004
0x0100000005 PUSH, 0x00000005
0x0600000006 EXTERN, "UnityEngineInput.__GetAxis__SystemString__SystemSingle"
0x0100000002 PUSH, 0x00000002
0x0100000001 PUSH, 0x00000001
0x0100000005 PUSH, 0x00000005
0x0600000007 EXTERN, "UnityEngineTransform.__Rotate__UnityEngineVector3_SystemSingle__SystemVoid"
0x0500ffffff JUMP, 0x00ffffff
この時のHeap
この内容はdebuggerでぶち抜いて、全部は確認してないので参考程度に
address(配列の添え字) | 内容物 |
---|---|
0x00 | System.Single(0) |
0x01 | UnityEngine.Vector3(0,0,0) |
0x02 | VRC.Udon.Common.UdonGameObjectComponentHeapReference(this) |
0x03 | VRC.Udon.Common.UdonGameObjectComponentHeapReference(this) |
0x04 | System.String(null) |
0x05 | System.Single(0) |
0x06 | System.String("UnityEngineInput.__GetAxis__SystemString__SystemSingle") |
0x07 | System.String("UnityEngineInput.__GetAxis__SystemString__SystemSingle") |
これから、Heapはまずdata領域を詰め、その後EXTERNの文字列を詰めていると思われる。
この時のEntryPoint
addressToSymbolだけ示す
address(配列の添え字) | 内容物 |
---|---|
0x00 | (Key, Value) = (0, Udon.Common.UdonSymbol(address:0, name:"_update", type: null)) |
EntryPointはcode領域のラベルを管理していると思われる。
この時のSymbolTable
addressToSymbolだけ示す
address(配列の添え字) | 内容物 |
---|---|
0x00 | (Key, Value) = (0, Udon.Common.UdonSymbol(address:0, name:"angle_0", type: System.Single)) |
0x01 | (Key, Value) = (1, Udon.Common.UdonSymbol(address:1, name:"axis_0", type: System.Single)) |
0x02 | (Key, Value) = (2, Udon.Common.UdonSymbol(address:2, name:"instance_0", type: System.Single)) |
0x03 | (Key, Value) = (3, Udon.Common.UdonSymbol(address:3, name:"Target", type: System.Single)) |
0x04 | (Key, Value) = (4, Udon.Common.UdonSymbol(address:4, name:"axisName_0", type: System.Single)) |
0x05 | (Key, Value) = (5, Udon.Common.UdonSymbol(address:5, name:"Single_0", type: System.Single)) |
SymbolTableはデータ領域のラベルのみ管理していると思われる。
気づいたこと
- EXTERN
- EXTERNの文字に適当なものをつめてもassemblyエラーは発生しなかった。突合せは実行時に行っているようにみえる
- EXTERNに使えるものは、前述のとおり実行時の判定になるのでアセンブリのみの分析からはわからなかった
- data領域
- 現状null/thisしかdefault値を渡せないらしいが、nullで初期化した該当型のインスタンスが生成されている様子。この辺C#に疎いから確信はない
- code領域
- 0xffffffが何かわからない
結論
- とりあえず今示されているアセンブリ命令のオペコード・オペランドは把握できた
- それぞれのラベルはすべて保存されているため動的に解決可能にみえるが、(ドキュメントを信用するなら)EXTERN以外のheapアドレスはバイトコードの段階で展開されている
- EXTERNは完全に動的解決
- 使えるAPIやcode領域のアドレス空間の予約状況はよくわからなかった
追記(2019/12/23)
madorama さんの調査によると、EXTERNのみで積み上げられると思った文字列は、他でも任意の文字列リテラルが積み上げられる様子。
詳しい調査はしていないので参考までに。