LoginSignup
18
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-20

これは?

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のみで積み上げられると思った文字列は、他でも任意の文字列リテラルが積み上げられる様子。

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

18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9