VRChat Udon VMのバイトコードを読んでみた

# これは？

VRChatでUdonというVMがつかえるようになったらしいのでアセンブリを分析して読んでみた。読めるとこだけ。

なお、調査には [cannorin](https://qiita.com/cannorin) 氏のtweet [Udon VM 向けのコンパイラを作ろうとしている[1]](https://togetter.com/li/1445282) を参考にした。

あと当方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で表現される

```C#
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](https://twitter.com/madorama_vrc) さんの調査によると、EXTERNのみで積み上げられると思った文字列は、他でも任意の文字列リテラルが積み上げられる様子。

詳しい調査はしていないので参考までに。