CEDEC2014 Live Coding in C++
-
Upload
seiya-ishibashi -
Category
Documents
-
view
9.602 -
download
3
Transcript of CEDEC2014 Live Coding in C++
Live Coding in C++Seiya Ishibashi
2014/09/02
Objective本公演が目指すところ● C++ で快適にゲームを開発する環境を
● いろんな黒魔術を駆使して
● できるだけ汎用的に
● できるだけ非侵入的に
● 一個人の労力で可能な範囲で
● 基本的に Windows 前提で
● 実現する
*本公演は私個人の実験の成果であり、 Unity とは特に関係はありません
(少なくとも今現在は。そういう話を期待していた方にはすみません )
About MeSeiya Ishibashi
● a.k.a i-saint (@i_saint)● CPU & GPU 全力でぶん回して美しいインタラクションを実現するのが生き甲斐
● 並列プログラミングを中心にローレベル全般を担当。たまにグラフィックも
● 最近だと Unity ちゃんステージの床を担当
Topics
● Runtime C++ Code Editing● State Save● Inspector
Topics
● Runtime C++ Code Editing● State Save● Inspector
Runtime C++ Code EditingRuntime C++ Code Editing ?
● C++ ソースの変更を実行中のプログラムにリアルタイムに反映させる機能
● いくつかの実装があり、順次解説
● Edit and Continue (Visual Studio)● Runtime Compiled C++● DynamicPatcher
Runtime C++ Code Editing● Edit and Continue (Visual Studio)● Runtime Compiled C++● DynamicPatcher
Edit and ContinueEdit and Continue ?● VisualStudio に備わっている機能
● 実行中にデバッガで止めて C++ ソースを編集すると、それを反映しつつ実行を継続できる
● 特定のコンパイルオプション (/ZI) をつけてビルドすることで対応可能
● ゲーム屋的に厳しい制限がいくつかある
○ 最適化が有効だと使えない
○ x64 未対応
○ デバッガで止めないと変更を反映できない
Runtime C++ Code Editing● Edit and Continue (Visual Studio)● Runtime Compiled C++● DynamicPatcher
Runtime Compiled C++Runtime Compiled C++● http://runtimecompiledcplusplus.blogspot.jp/● Doug Binks 氏作
● 多くの採用実績がある
● Unreal Engine 4 の Hot Reload は大体これと同じ仕組み
Runtime Compiled C++実装戦略
1. インターフェース class を定義し、編集可能にしたい部分を継承した class に閉じ込め、DLL に分離
2. C++ ソースを更新したら DLL をビルド
3. 対象 DLL に属するオブジェクトをシリアライズし、 DLL をリロードし、オブジェクトをデシリアライズ
Runtime Compiled C++
// main.exe
class Interface{public: virtual void Update()=0; virtual void Serialize(...)=0;};
// entity.dll
class Entity : public Interface{public: virtual void Update(); virtual void Serialize(...);};
Runtime Compiled C++
// main.exe
class Interface{public: virtual void Update()=0; virtual void Serialize(...)=0;};
// entity.dll
class Entity : public Interface{public: virtual void Update(); virtual void Serialize(...);};
// entity_updated.dll
class Entity : public Interface{public: virtual void Update(); virtual void Serialize(...);};
Runtime Compiled C++
// main.exe
class Interface{public: virtual void Update()=0; virtual void Serialize(...)=0;};
// entity.dll
class Entity : public Interface{public: virtual void Update(); virtual void Serialize(...);};
// entity_updated.dll
class Entity : public Interface{public: virtual void Update(); virtual void Serialize(...);};
Runtime Compiled C++
DLL への分割● interface class を用意
● 編集可能にしたい単位で DLL に分割 (≒プロジェクトを分割 )● DLL 側は interface を継承した class を実装し、その factory 関数を exe 側に提供
Runtime Compiled C++
DLL のビルド● VisualStudio のコンパイラを呼ぶ
○ レジストリから情報を得て cl.exe を起動
Runtime Compiled C++
DLL のリロード1. DLL に属するオブジェクトをシリアライズ
2. 新しい DLL をロード
3. 新しい DLL でオブジェクトを再生成し、デシリアライズ
4. 古いオブジェクトを破棄
5. 古い DLL をアンロード
● シリアライズはデータ構造に変更がなくても必要
○ そうしないと vtable が更新されず、古い DLL の関数を呼びに行こうとして死ぬ
Runtime Compiled C++pros:
● 実装がシンプルかつ堅実
● 多くのプラットフォームで実現可能
● 最適化が有効でも機能する
● 編集後もデバッガで追跡可能
cons:● 編集可能にしたい部分を DLL に分離する必要がある
● シリアライズが必要
● interface を継承した class しか更新できない
Runtime C++ Code Editing● Edit and Continue (Visual Studio)● Runtime Compiled C++● DynamicPatcher
DynamicPatcher
DynamicPatcher● https://github.com/i-saint/DynamicPatcher● Runtime Compiled C++ にインスパイアされて作りました
● 既存のプロジェクトに簡単に組み込めることを再優先に設計
● Riot Games で採用された実績あり
DynamicPatcher実装戦略
1. C++ ソースをコンパイル
2. 生成された .obj ファイルを自力でロード&リンク
3. 古い関数を新しい関数への jmp に書き換えて更新
DynamicPatcher
// main.exe
class Entity{public: virtual void Update();};
DynamicPatcher
// main.exe
class Entity{public: virtual void Update();};
// entity.obj
class Entity{public: virtual void Update();};
DynamicPatcher
C++ ソースをコンパイル● msbuild に任せる
○ VisualStudio のビルドツール
○ ターゲットを “ClCompile” とすることでコンパイルだけ実行可能
DynamicPatcher
.obj ファイルのロード&リンク● .obj はフォーマットが公開されており、比較的わかりやすい構造をしているため、自力ロード&リンク
はそこまで難しくはない
○ ファイルフォーマット資料: http://www.skyfree.org/linux/references/coff.pdf
DynamicPatcher
section を再配置しつつメモリ上にマップ● .obj ファイルは section と呼ばれるブロックで構成される
● section 毎に色んな属性と情報が付随する
○ データ、実行コード、デバッグ情報、 etc● アライメント指定がある section があり、.obj ファイルの状態ではこれを考慮した配置になっていな
い。自力で再配置する必要がある
○ これを怠ると __m128 の literal を参照などで謎のクラッシュが起きる
● VirtualAlloc() で確保した、実行可能属性付きの領域に section の内容を移していけば ok
DynamicPatcher
relocation 情報を元にシンボルをリンク● relocation 情報: リンク時にここにあのシンボルのアドレスを書き込んでね、という情報
● この情報に従ってアドレスを書き込んでいけばリンクが完了する
● .obj 内にあるシンボルは .obj のシンボルテーブルから取得可能
● ホストプログラムのシンボルは SymFromName() もしくは .map ファイルから取得可能
○ SymFromName() は .pdb が必要かつ超遅い上、 thread unsafe○ .map ファイルを使う方が望ましい (ただしリンカオプション /MAP が必要)
● 特定のシンボルは常にホストプログラムのシンボルでリンクする必要がある
○ static なオブジェクトなど、分散されると困るもの
DynamicPatcher
古い関数を新しい関数への jmp に書き換えて更新● 関数の先頭 5 byte を新しい関数への jmp に書き換える
○ x86 には命令自身に飛び先アドレスを含められる jmp 命令がある
○ レジスタの内容を変えずに制御を飛ばせるため、引数が同じ型の別の関数に簡単にリダイレク
トできる
● 関数のアドレスは変わらないので vtable の更新が必要なくなる
○ シリアライズなしで class の挙動を変更可能
● virtual 関数に限らずほとんどの関数の更新が可能
○ inline 関数など一部例外あり
DynamicPatcher
既存のプログラムに組み込む● main() 関数に 1 行足してビルドしてもらえるならそれで事足りる
● しかしソースに変更が必要だと導入コストが上がる。ソースの変更なしで対応したい
● こういう時こそ DLL Injection
DynamicPatcher
DLL Injection● 既存のプログラムに任意の DLL (=任意のコード) を注入するテクニック
● CreateRemoteThread() を用い、対象プロセスの中で LoadLibrary() を呼ばせる
○ VirtualAlollocEx() で対象プロセス内にメモリを確保してロードさせたい DLL のパスを書き込
み、それを引数として LoadLibrary() をエントリポイント関数としてスレッドを作成
● わりといろんなツールで用いられている
○ ビデオキャプチャソフト、グラフィックデバッガ、 etc
DynamicPatcher
既存のプログラムに組み込む (2)● 一連の機能を実装した DLL を対象プロセスに注入
● DLL からプロセス間通信で外部から通信する窓口を開く
● リクエストに応じて更新する関数の指定や .obj ファイルをロードなどを行う
● 今回は VisualStudio のアドインを作成し、対象プロセスと通信するようにした
○ 以下の機能を実装
1. DLL Injection しつつプログラム起動
2. .cpp をコンパイルしてロードリクエストを送る
3. 更新するシンボルを指定
DynamicPatcher
demo
DynamicPatcher
制限&注意点● 変更後の .cpp はデバッガで追えなくなる
○ ソースとバイナリは変わる一方デバッグ情報は変わらないため
● /LTCG (リンク時コード生成 ) オプションでコンパイルされた .obj は対応不可 ○ 通常と異なるファイルフォーマットになるため
● /GR (RTTI 有効) でコンパイルされた .obj は危険
○ vtable の構造が変わる
● global オブジェクトのコンストラクタ問題
○ atexit() でデストラクタを呼ぶ処理を登録するため危険
● 例外
○ 対応難度高し
DynamicPatcher
pros● 既存のプロジェクトにそのまま適用可能
● 最適化が有効でも機能する (ただしリンク時コード生成はダメ )● ほぼ全ての関数を更新可能
cons● 編集後デバッガで追えなくなる
● 対応可能なプラットフォームに大きな制限がある
● 色々不思議な制限がついてまわる
Runtime C++ Code Editing
考察● Edit and Continue
○ x64 対応 & 最適化有効がないとゲーム屋的に厳しい …● Runtime Compiled C++
○ 信頼性の高さは採用実績が証明済み
○ しか既存のプロジェクトに組み込むのは大変
○ 実装の際はビルドツールなど周辺環境の整備の方が大変だと予想される
● DynamicPatcher○ 導入コストの低い&適用範囲の広い
○ ただし色々不思議な制限がついてまわる
○ 改良次第で制限緩和できそうだが、実装は大変でプラットフォーム依存性も高い
Runtime C++ Code Editing
補足情報● Recode
○ http://www.indefiant.com/○ GDC 2014 で発表。Cryengine が採用
○ 既存プロジェクトにそのまま適用可能。マニュアルから推測するに DynamicPatcher 方式?
● libdcompile○ https://github.com/Fadis/libdcompile○ clang & LLVM を用いて C++ で eval を実現するライブラリ
● Projucer IDE○ http://2013.cppnow.org/session/the-projucer-live-coding-with-c-and-the-llvm-jit-engine/○ clang & LLVM JIT engine を内蔵した IDE
Topics
● Runtime C++ Code Editing● State Save● Inspector
State SaveState Save?● プロセスの内部状態をまるごと保存&復元する機能
● Checkpointing という名前がより正式らしい
○ http://en.wikipedia.org/wiki/Application_checkpointing● 適当な間隔でセーブしながらテストプレイ ->直したいところがあったら巻き戻し、修正し、プレイ継続、
という使い方を想定
○ TAS 動画製作手法のゲーム制作への応用
○ TAS の場合巻き戻してプレイを修正するが、この場合レベルそのものを修正する
● 通常 StateSave はタイトルごとに実装するが、大きな手間がかかる。汎用的に実現できないか?
○ PC に限定すればたぶん可能!
State Save実装戦略● プロセスの状態を復元するのに必要なものは以下の 3 つ
○ メモリの状態
○ スレッドの状態
○ カーネルオブジェクトの状態
● これらの復元に必要な情報を収集する
State Save予備知識: API Hook● 関数の呼び出しを別の関数にリダイレクトさせるテクニック
● 対象が外部 DLL の関数の場合、 Import Address Table を書き換えることで容易に実現可能
○ 各モジュールには外部 DLL の関数の名前とアドレスを保持する領域がある
○ そのアドレスを書き換えることで呼び出しをリダイレクトさせることができる
hoge.exe
MSVCR.dllImportNameTable ImportAddressTableprintf 0x40000000exit 0x40000020...
MSVCR.dll
0x40000000 printf()0x40000020 exit()...
State Save予備知識: API Hook● 関数の呼び出しを別の関数にリダイレクトさせるテクニック
● 対象が外部 DLL の関数の場合、 Import Address Table を書き換えることで容易に実現可能
○ 各モジュールには外部 DLL の関数の名前とアドレスを保持する領域がある
○ そのアドレスを書き換えることで呼び出しをリダイレクトさせることができる
hoge.exe
MSVCR.dllImportNameTable ImportAddressTableprintf 0x50000000exit 0x50000020...
MSVCR.dll
0x40000000 printf()0x40000020 exit()...
Injected.dll
0x50000000 printf_hook()0x50000020 exit_hook()...
State Save予備知識: API Hook (2)● WinAPI を hook して
○ 復元に必要な情報をかすめ取る
○ もしくは復元可能な独自ルーチンに差し替える
● というのが今回の基本戦略
State Saveメモリの状態
● モジュール領域、ヒープ領域、ページメモリ、スタック領域、個別対処が必要
State Saveメモリの状態 (2)● モジュール領域
○ exe や dll がマップされた領域
○ global 変数、static 変数はこの領域に存在
○ コード領域は書き込み不可能、変数領域は書き込み可能属性がついている
○ モジュールの先頭から VirtualQuery() で順次メモリを調べ、書き込み可能な領域を保存
○ モジュールの巡回は CreateToolhelp32Snapshot(), Module32First(), Module32Next()
State Saveメモリの状態 (3)● ヒープ領域
○ malloc() や new によって確保された領域
○ これらはそのままでは確保する領域のアドレスの予測が困難
○ MSVCRT のメモリ確保ルーチンは全て WinAPI の HeapAlloc() で実装されている
○ HeapAlloc() を API hook で乗っ取って独自ルーチンに差し替えることで対応可能
○ 今回の例では事前にでかいメモリ領域を確保して dlmalloc で管理するルーチンを使用
State Saveメモリの状態 (4)● ページメモリ
○ VirtualAlloc() 一族で確保された領域
○ アドレス指定の確保ができるため簡単
○ VirtualAlloc() 一族を hook して必要な情報を記録するだけ
State Saveメモリの状態 (5)● スタック領域
○ GetContext() でスレッドのレジスタの状態を取得できる
○ esp (x64 だと rsp) レジスタがスタックのどこかを指している
○ VirualQuery() で esp/rsp の領域の開始アドレスとサイズを取得して記録
○ スレッドの巡回は CreateToolhelp32Snapshot(), Thread32First(), Thread32Next() を使用
■ 全プロセスの全スレッドを巡回する点に注意
State Saveスレッドの状態● 各スレッドのスタックとレジスタの状態
● スタックについては先に触れた通り
● レジスタの内容は GetContext() & SetContext() を呼ぶだけ
State Saveカーネルオブジェクトの状態
● 非常に難しい部分
● API Hook で超頑張って復元に必要な情報を収集
● HANDLE は独自管理のもので wrap○ WinAPI が返す HANDLE は値は予測困難なため
● DirectX / OpenGL のオブジェクトなども対応が必要
● 対応しなくてもなんとかなるモジュールは無視するのも手
○ API hook せず、モジュール領域のメモリやスレッドもノータッチ
State Save既存のプログラムに組み込む
● DLL Injection で簡単に実現可能
● 今回の例では DLL の中で特定キー入力に応じてセーブ&ロード
State Savedemo
State Save考察● ちゃんと動作すれば強力な開発支援機能になるはず
● しかしちゃんと動作するものに仕上げるのは非常に難しい
● 今回の例もまだまだ発展途上
○ カーネルオブジェクトはほとんど未対応。グラフィック系も未対応
○ しかし特定シーンに限定すれば使えなくもなさそう
● プロセスの再生成は今回は諦め
○ ASLR によりメモリレイアウトの再現が困難なため
○ WindowsXP SP3 以降のセキュリティ機能がたまにローレベルプログラミングを阻害する
State Save補足情報: HourGlass
● https://code.google.com/p/hourglass-win32/● オープンソースの Windows 用 TAS 動画作成支援ツール
● API Hook による内部ステートの保存、入力データの再現、動画撮影機能などを実装
● ただし 32 bit の WindowsXP でないとまともに動かない
● ソースコードはとても面白く参考になる
State Save補足情報: undump
● http://d.hatena.ne.jp/shinichiro_h/20060715/1152922272● Linux 上で今回説明した内容を実現するもの
● Linux ではいくらか Windows より楽に実現できる様子
Topics
● Runtime C++ Code Editing● State Save● Inspector
InspectorInspector ?
● GUIからリアルタイムにデータを編集する機能
● いまどきのゲームエンジンなら大抵備わってるアレ
Inspector実装戦略
● デバッグ情報に class のデータ構造が入っているのでそれを利用
● オブジェクトへのポインタと型名から編集用 GUI を構築
● GUI の編集結果を反映
Inspectorデバッグ情報のパース
● 型名 (文字列) から SymGetTypeInfo() で型情報を取得
UDT
class Hoge{public: int m_data;};
Index = 15
UdtKind = UdtClassName = “Hoge”Length = 4
Data
Index = 16
Type = 17Name = “m_data”
BaseType
Index = 17
Type = btIntLength = 4
child
Inspectorデバッグ情報のパース (2)● n byte 目にどの型のデータがある、といった情報が取れる
● オブジェクトの各メンバに対応する GUI のコントロールを作成
○ 頑張ればデバッガの変数欄をそのまま再現できるはず
● メンバ関数も情報取れる
○ 関数を呼ぶコントロールの自動生成も可能なはずだが、非常に難しい
Inspector手動生成と組み合わせる● 自動生成オンリーは少々厳しい
○ std::vector 問題
○ 編集させたくないメンバ問題
○ 命名規則を設けて annotation の代わりにする、などはありかも
● 手動生成で補う
● 関数を呼ぶ GUI も手動生成なら実装は簡単
Inspectorエディタの実装● GUI フレームワークはなんでもいいが、今回は HTML & Javascript を使用
● HTTP サーバは Poco のおかげで容易に実装可能
○ http://pocoproject.org/● ゲームからブラウザに一定間隔毎に json 形式でデータを serve● ブラウザから送られてきたフォームデータをパースしてデータ更新
Inspectordemo
Inspector考察● 比較的お手軽に実装可能でありながら恩恵は大きい
● ソースに手を加えずに機能追加したい場合やや難度が上がる
○ DLL Injection & class のコンストラクタ & デストラクタを hook ○ 今回の例はソースに手を加える形で実装
Inspector補足● Unreal Engine 4 は別の実装アプローチ
○ デバッグ情報使わず自力解析
Conclusion● デバッグ情報と実行可能メモリさえあれば C++ は動的言語
● 特定 OS & コンパイラ前提であれば色々な不思議機能を実現可能
● 既存のツールからインスピレーションを得られることも
Questions?
Endありがとうございました!
Resources● 今回のデモのソースコード群
○ DynamicPatcher: https://github.com/i-saint/DynamicPatcher○ RestoreProcessState: https://github.com/i-saint/scribble/tree/master/RestoreProcessState○ WebDebugMenu: https://github.com/i-saint/WebDebugMenu○ atomic: https://github.com/i-saint/atomic
● 様々な関数 hook の実装: http://i-saint.hatenablog.com/entry/2013/07/19/205539