runtime c++editing
TRANSCRIPT
Runtime C++ Editingby i-saint
Who am I ?
i-saint / Seiya Ishibashi ( @i_saint )● ゲーム屋さん (コンソール系)● 仕事では主にシステム&ローレベルプログラミングを担当
● 趣味でグラフィックスとかも
● CPU を全コア無駄なく全力でぶん回すのが生き甲斐
Background Story
ゲーム開発ではよくスクリプト言語が用いられる。これには大き
く 2 つの目的がある。
● トライ&エラーのサイクルを早める○ ゲーム開発では極めて重要
○ プログラムを再起動せずに変更を反映できる機能はほぼ必須要件
● 非プログラマがゲームの挙動を変更できるようにする○ チーム開発ではこちらも重要
○ ノベルゲームなどの分野では主にマークアップランゲージが用いられ、専属
のプログラマがつくことは稀、という状況
Background Story
総合型ゲームエンジンの登場
● 総合型ゲームエンジン○ 乱暴に説明すると、ゲーム用のフレームワーク兼 IDE のようなもの
○ レンダリングエンジン、スクリプトエンジン、レベルエディタ、リソースマネー
ジャなど、ゲーム開発で必要とされる一般的な仕組みを用意
○ Unity 3D や UnrealEngine が有名
Background Story
総合型ゲームエンジンの登場により状況にやや変化
● 非プログラマがゲームの挙動を変更○ この要件はビジュアルスクリプトツールで解決するのが最近のトレンド
○ UnrealEngine3 の Kismet など
● トライ&エラーのサイクルを早める○ C/C++ でできるならその方が都合がいいことも多い
○ 実行中に C++ ソースを変更してリアルタイムに反映できたら理想的
○ UnrealEngine4 がこれを実現。ゲーム屋の間に衝撃が走る
UnrealEngine4 登場後、ゲーム屋の間で実行時 C++ 編集がホットなトピックに!
Runtime C++ Editing
いくつかの公開されている実装がある
● Edit&Continue● libdcompile● RuntimeCompiledC++● DyamicPatcher
Runtime C++ Editing
● Edit&Continue● libdcompile● RuntimeCompiledC++● DyamicPatcher
Edit & Continue● VisualC++ の強力な機能● 実行中にデバッガで止めて C++ ソースを編集すると、それを反映しつつ実行を
継続できる● 特定のコンパイルオプション (/ZI) をつけてビルドするだけで対応可能● 最適化が有効だと使えない、なぜか x64 非対応、などの制限あり
○ 最適化有効だと使えないのはゲーム屋的に痛い○ x64 非対応も次世代機を考えるといずれ問題になるはず
強力だが、使える状況が限定されている
Runtime C++ Editing
● Edit&Continue● libdcompile● RuntimeCompiledC++● DyamicPatcher
libdcompilehttps://github.com/Fadis/libdcompile
(解説: http://www.slideshare.net/fadis/libdcompile )
@fadis_ さん作。C++ で eval() を実化するライブラリ
1. LLVM & clang を内蔵
2. clang でコンパイル
3. LLVM でリンク & 実行
libdcompile● Windows 非対応なため今回は未検証…● LLVM を使ったものとしては他に Projucer という IDE がある
○ http://www.youtube.com/watch?v=imkVkRg-geI○ C++ の編集をリアルタイムに反映できるのに加え、ツール上で UI を編集し
たらそれが C++ に反映される、双方向編集を実現している
● LLVM 内蔵アプローチは応用範囲が広く、夢が広がる
Runtime C++ Editing
● Edit&Continue● libdcompile● RuntimeCompiledC++● DyamicPatcher
RuntimeCompiledC++http://runtimecompiledcplusplus.blogspot.jp/UnrealEngine4 発表前からあったらしいが、UE4 登場後に広く知られるようになった代物
1. インターフェース class を定義し、編集可能にしたい部分を継承した class に閉じ込め、DLL に分離
2. C++ ソースを更新したら DLL をビルド3. 対象 DLL に属するオブジェクトをシリアライズし、DLL をリロードし、オブジェクト
をデシリアライズ
RuntimeCompiledC++
● DLL のビルド○ ごく普通に VisualC++ のコンパイラ (cl.exe) を呼んでいる
● DLL に属するオブジェクトのシリアライズ○ 面倒だがデータ構造に変更がなくても必要○ そうしないと vftable が更新されず、古い dll の関数を呼びに行こうとして死
ぬ
RuntimeCompiledC++
pros:● 実装がシンプルかつ堅実● 多くのプラットフォームで実現可能
cons:● class の更新に serialize が必須● DLL 毎にプロジェクトを分離するのが面倒
UnrealEngine4 の HotReload はこれと同じようなアプローチだと推測される。
RuntimeCompiledC++
pros:● 実装がシンプルかつ堅実● 多くのプラットフォームで実現可能
cons:● class の更新に serialize が必須● DLL 毎にプロジェクトを分離するのが面倒
こやつらをなんとかしたい。もっとお手軽に使えるようにしたい。
Runtime C++ Editing
● Edit&Continue● libdcompile● RuntimeCompiledC++● DyamicPatcher
DynamicPatcherhttps://github.com/i-saint/DynamicPatcher今回の話のメインディッシュ。
RuntimeCompiledC++ に不満があった&面白そうな別のアプローチを思いついたので作ってみました。
1. C++ ソースを更新したらコンパイル2. .obj ファイルを自力でロード&リンク3. 古い関数を新しい関数への jmp に書き換えて更新
DynamicPatcher
1. C++ ソースを更新したらコンパイル2. .obj ファイルを自力でロード&リンク3. 古い関数を新しい関数への jmp に書き換えて更新
DynamicPatcher
1. C++ ソースを更新したらコンパイル
● 更新の監視○ 専用スレッドで FindFirstChangeNotification()○ ここは RuntimeCompiledC++ と同じ
● コンパイル○ msbuild を呼ぶだけ。GNU 系ツールで言うところの make○ .vcxproj ファイルは makefile の機能も果たしている○ コンパイルだけしたい場合 “/target:ClCompile” を指定
DynamicPatcher
1. C++ ソースを更新したらコンパイル2. .obj ファイルを自力でロード&リンク3. 古い関数を新しい関数への jmp に書き換えて更新
Load & Link .obj Files.obj はフォーマットが公開されており、比較的わかりやすい構造なため、自力でロード&リンクして中にある関数を実行できるようにするのはそこまで難しくはない。
(資料 http://www.skyfree.org/linux/references/coff.pdf )大雑把には以下の手順
1. ファイルの内容を section を再配置しつつメモリ上にマップ2. relocation 情報を元にシンボルをリンク
Load & Link .obj Files
1. section を再配置しつつメモリ上にマップ● .obj ファイルは section と呼ばれるブロックで構成される● section 毎に色んな属性と情報が付随する
○ 読み取り専用データ、実行コード、デバッグ情報、etc● align 指定がある section があり、.obj ファイルの状態では align を考慮した配
置になっていない。自力で再配置する必要がある○ これを怠ると __m128 の literal を参照とかで謎のクラッシュが起きる○ VirtualAlloc() で確保した、実行可能属性付きの領域に section の内容を
移していけば ok
Load & Link .obj Files
2. relocation 情報を元にシンボルをリンク● relocation 情報: リンク時にここにあのシンボルのアドレスを書き込んでね、とい
う情報● この情報に従ってアドレスを書き込んでいけばリンクが完了する● .obj 内にあるシンボルは .obj のシンボルテーブルから見つけられる● ホストプログラムのシンボルは dbghelp の SymFromName() で取得
○ .pdb が必要になる○ この API は猛烈に遅く、最初のリンクはやや時間がかかってしまう
● 特定のシンボルは常にホストプログラムのシンボルでリンクする機構も用意○ singleton の getInstance() などで必要になる
Load & Link .obj Files
制限事項● /LTCG (リンク時コード生成) オプションでコンパイルされた .obj は対応不可
○ 通常と異なるファイルフォーマットになるため○ VisualC++ のツールチェイン (dumpbin) ですら情報を出せない
● /GR (RTTI 有効) でコンパイルされた .obj は危険○ vftable の構造が変わる○ .obj の時点では最初の要素が RTTI 情報へのポインタになっている○ 通常実行時は 1 個前にずれて -1 番目が RTTI 情報になっている○ 再現する方法がわからず未対応○ 幸いゲーム屋は RTTI 使わないことが多い
● global オブジェクトのコンストラクタ問題○ atexit() でデストラクタを呼ぶ処理を登録するため危険。非対応。
● デバッガでソースを追えない○ かなしい
DynamicPatcher
1. C++ ソースを更新したらコンパイル2. .obj ファイルを自力でロード&リンク3. 古い関数を新しい関数への jmp に書き換えて更新
Patching Functions
古い関数を新しい関数への jmp に書き換えて更新● 関数の先頭 5 byte を新しい関数への jmp に書き換える
○ x86 には命令自身に飛び先アドレスを含められる jmp 命令がある○ これを使うとレジスタの内容を変えずに制御を飛ばせるため、同じ型の引
数の別の関数に簡単にリダイレクトさせることができる● 関数のアドレスは変わらないので vftable の更新が必要なくなり、シリアライズ
なしで class の挙動を更新しつつ実行継続できる○ データ構造が変わる変更はさすがに無理。ユーザー側の対応が必要○ シリアライズとか事前に余剰スペースを設けてなんとかするとか
Patching Functions
古い関数も呼べるようにする● hook 的に使いたいことがたまにあるため対応● 更新前の 5 byte を含む命令を別の場所に退避させ、末尾に元の場所への jmp
を書き加えておく○ このコード片を call すれば更新前の関数が実行される○ これを実装するのは結構しんどい。x86 は命令が可変長なので、命令を正
しく解釈して 5 byte を含む命令がどこまでか調べる必要がある○ 相対アドレスを含む命令を含む場合つなぎ変えも必要○ 幸い命令の解釈はライブラリ (tDisasm) があったのでそれを使用した
● MHook の実装が参考になる○ http://codefromthe70s.org/mhook23.aspx○ tDisasm は MHook に含まれる
Patching Functions
その他注意点● x64 の場合、相対アドレスが 32bit を超えるケースへの対処が必要
○ 64bit absolute indirect jump なる命令があるので、これをトランポリンコードとして挟む■ 古い関数->トランポリン->新しい関数
○ 古い関数に直接 64bit absolute indirect jump を書き込んでもいいが、14 byte 必要なためやや危険
● .obj をロードした時どの関数を更新するか?○ 問答無用で全部更新するのは危険だし無駄が多い○ dllexport は .obj に情報が残るのでそれを利用○ dllexport (を包んだマクロ) をつけたシンボルを自動更新するという仕様に○ プログラム開始時点でついてる必要はないため、多少ユーザーに手間をか
けることになるが致命的な制限にはならないと判断
DynamicPatcher
pros:● ほとんど前準備なしに実行時C++編集可能
cons:● たぶん x86(64) 限定
○ レジスタの内容変えず jmp できる特性から
● obj ロードによる制約○ デバッガでソースを追えない○ 規模が大きくなるとリンク時間が長くなる○ /LTCG 禁止などの奇妙な制限
お手軽に使えるのは強力なメリットだが、制限がやや痛い。
Dealing with .dll
dll との併用● .obj の制限は .dll なら解決できる
○ デバッガでソース追えない -> dll なら可能○ リンク遅い -> dll ならビルド時に完了してる○ 奇妙なコンパイルオプション制限 -> dll なら必要なし
● .obj も .dll も透過的に扱えれば状況に応じて使い分けができる● プロジェクト分離する手間はかかるが、どちらにせよ規模が大きくなってきたら
dll への分離が必要に迫られることが多い○ ビルド & リンク時間削減、リンク時メモリ使用量削減などのため
Dealing with .dll
dll 対応● ExportAddressTable を巡回すれば dllexport なシンボルを巡回できる● あとは .obj 同様、古い関数の先頭を新しい関数への jmp に書き換えるだけ
○ dll->dll の関数書き換えだと .pdb すら不要● RuntimeCompiledC++ と違い、シリアライズ不要の恩恵は残る
○ 関数のアドレスは変わらない、という違いから● ここまでは簡単だが…
Dealing with .dll
ファイルロック問題● ロードされている dll は対応する pdb と共にロックがかかる
○ ビルドしてできた dll をそのままロードすると、pdb へ書き込めなくなって以降のビルドが失敗する
○ この問題を回避するため、ロードする前に dll & pdb を適当にリネーム&コピーする必要がある
○ このとき、dll の中にある pdb へのフルパスと GUID、および pdb の中にある GUID を更新する必要がある■ GUID についてはやや複雑な事情があるが、こうしないとデバッガが
正しいシンボル情報を読めないことがある● この対応が大変
Dealing with .libついでに .lib にも対応
● .obj が数珠つなぎになっただけの構造なので簡単○ http://hp.vector.co.jp/authors/VA050396/tech_04.html
● インポートライブラリは未対応だが、dll 直接読めるので無問題
DynamicPatcher
pros:● ほとんど前準備なしに実行時C++編集可能
cons:● たぶん x86(64) 限定
○ レジスタの内容変えず jmp できる特性から
● obj ロードによる制約○ デバッガでソースを追えない○ 規模が大きくなるとリンク時間が長くなる○ /LTCG 禁止などの奇妙な制限
.obj ロードのお手軽さはそのまま、必要に応じて .dll に切り替えることも可能に。
DynamicPatcher今後こんなことができたらいいなリスト
● .obj のリンク高速化○ .pdb を自力で解釈するとか
● .obj のままデバッガでソース追う○ 不可能ではないと思う…けど難しそう
● POSIX 系 OS 対応○ できるはず
● ARM 対応○ 不可能?求む情報
Conclusion
C++ は動的言語 (デバッグ情報が使えれば)
End
thank you for watching!
Supplement
発表後の質問等を受けての追記
● 関数書き換え時にスレッド止めたりの対応はしてる?○ 現状やってないが、やったほうがいいのは確か○ メインループのどこかで更新関数を呼ばせる構造になっているため、大抵
のケースでは問題は起きない
● .obj 内の例外処理○ 例外処理は全く考えてなかったが、対応は一筋縄ではいかないらしい○ 幸いゲーム屋は例外ほとんど使わない!
● /hotpatch はいかが?○ 諸々の事情により使用を見送った○ ユーザーにこのオプションを要求したくない、/hotpatch 未対応の既存の
コードを書き換えたいケースもある、x64 だと /hotpatch 使っても hook 処理は簡単には実現できない、等など
● ARM 対応○ 可能らしい○ register 全部 push とかの命令がある