CEDEC 2014 / Status Report
CEDEC 2014 で喋ることになりました。
Live Coding in C++
ここ 1 年くらい趣味でやってた活動をまとめたような内容にする予定で、実行時 C++ 編集ネタとプロセスの状態保存/復元の 2 つが主なトピックになります。タイトルから推測できる通り、色んな黒魔術を駆使して C++ で快適に開発しよう、という趣旨です。
あと、先月また転職して現在 Unity Technologies Japan に勤めています。Unity を自分のようなオールドタイプでスピード狂なプログラマーでも快適に扱えるゲームエンジンに鍛えるべく奮闘中です。(ちなみに上記 CEDEC の私の講演に Unity の話は出てきません)
今回転職にあたって色んな人のお世話になりました。相手してくださった方々本当にありがとうございます。
ここ数ヶ月在職中の鬱憤を晴らすべくあっちこっち遠出しまくってたので、以下適当な記録。誰得写真集。
4/4。雨上がりの夕刻にものすごく綺麗な夕日が見れた一日でした。
奥多摩 / 奥多摩湖 / 日原鍾乳洞
首都圏から手軽に行けて都会の喧騒を忘れられるいい場所で、度々足を運んでいます。
猿とか鹿とかに出くわすこともあり、クマ注意の看板まであって、東京都の違う側面が見られます。
戦場ヶ原 / 日光
自転車で行ける面白そうな場所を適当に探してたら目に付いたので行ってみたんですが、大当たりでした。雪の残る湿原はかなり見た目にファンタジー度が高く、周囲の地形も変化に富んでいて見てて飽きません。別の季節にまた訪れてみたいところです。
霧ヶ峰 / 八島ヶ原湿原
戦場ヶ原が素晴らしかったので他の湿原も見てみよう…と行ってみた場所。とにかくだだっ広い感じが素晴らしくて、眼前一面に広がる笹野原は圧巻でした。
ただ、観光地として整備されすぎてて雰囲気ぶち壊しになってる部分も多いのがやや残念でした。そこらじゅうに張り巡らされた柵と警告の看板とか…。
熊本 / 阿蘇
福岡に遊びに行ったついでに立ち寄り。火口付近は植物がほとんど無い荒涼とした景色が広がり、その外側には牧草地帯、更に外側には外輪山と、変化に富んだ植生が面白い場所でした。
上海
出張で行ってきました。中国自体初めてだったんですが、ネットに規制がかかってて Google とか Twitter とか有名なサービスの多くは遮断されるとか、みんな信号無視しまくりで横断歩道が恐怖そのものだったりとか、めちゃくちゃでかい街だけど細部が色々雑な感じとか、色々カルチャーショックを味わえました。
悪い方向の印象が強かったものの、寺院とか植生は見てて面白くて、中国の田舎の方を旅するのは楽しそう、と思わせてくれました。
restore process state
プロセスの内部状態を保存して後で復元したい、ということがゲーム開発ではよくあります (いわゆる state save)。チェックポイントから再開みたいな機能の実現の他、あると開発中何かと役に立ちます。定期的に state save しつつ、気になったところがあったら巻き戻して Alcantarea の実行時コード編集で改良、という TAS みたいなやりかたが最近の私の開発スタイルです。
通常は boost::serialization とかを使って必要なデータを serialize する処理を書いてタイトル毎に実装しますが、これをもっと簡単に、汎用的に、できれば既存のプログラムに外部から適用できる形で実現する方法はないか、と夢見た方も多いんじゃないかと思います。これを実現できるかもしれない方法を思いついて、ある程度のところまで成果を出せたので経過を書き残します。
(Virtual Machine 使うのが最も確実な方法ではありますが、大げさすぎるので今回なしの方向で)
汎用 state save は、以下の 3 つを保存して復元できれば実現できそうな気がします。
- メモリの状態
- スレッドの状態
- カーネルオブジェクトの状態
メモリの状態とスレッドのレジスタの状態が完全に再現できればポインタ、プログラムカウンタ、リターンアドレスなどがそのまま有効になり、実行を再開できるはずです。ファイルや mutex などのカーネルオブジェクトも多くの場合復元が必須になるでしょう。
今回は前提として state の有効期限はプロセスの生存期間中のみとします。プロセスを再生成するところまで考慮すると厄介な障害が増えるためです。 基本戦略は対象プロセスに DLL Injection で独自のコードをねじ込み、主要な WinAPI を API hook で乗っ取って復元可能にする独自ルーチンを挟む、というものです。
- メモリの状態
メモリにはモジュール、ヒープ、ページメモリ、スタック、TLS などがあり、それぞれ別の対処が必要です。
・モジュール
メモリにマップされた exe や dll の領域です。プロセスの再生成までやる場合、これの復元が非常に難しいチャレンジになると予想されますが、今回そこは考えません。しかし global 変数や関数内 static 変数なんかはこのモジュール領域内にあるため、これらは保存&復元が必要になります。
CreateToolhelp32Snapshot(), Module32First(), Module32Next() でロードされている全モジュールの詳細な情報を取得できます。これでモジュールの領域を取得し、VirtualQuery() で順次属性を調べて書き込み可能になってる箇所を探して保存します。
・ヒープ
malloc() や new で動的に確保される領域です。これらが確保する領域のアドレスは予測が難しく、そのままでは正確なメモリレイアウトの復元は困難です。このためちょっとしたハックが必要になります。過去に何度か触れたように、Windows の CRT の malloc() は最終的に HeapAlloc() に行き着きます。なので HeapAlloc() を乗っ取って独自のメモリ管理ルーチンに差し替えれば malloc() を復元可能にできます。
今回の例では、初期化時に巨大な領域を確保してその領域を dlmalloc で管理、保存するときはその領域をまるごと書き出しています。
・ページメモリ
VirtualAlloc() で確保された領域です。ヒープと違ってアドレス指定の確保ができるため、メモリレイアウトの復元は簡単です。
今回の例では VirtualAlloc()/VirtualFree() を hook して領域を記録し、それをまるごと保存&復元しています。
・スタック
各スレッドのスタック領域で、関数内ローカル変数やリターンアドレスがある領域です。どちらかというとスレッドの状態に属する話です。 GetContext() でスレッドのレジスタの状態を取得でき、esp (x64 なら rsp) がそのスレッドのスタックのどこかを指しているので、そこを VirtualQuery() で調べることでベースアドレスとサイズを取得できます。あとはその領域をまるごと保存するだけです。
スレッドの巡回には CreateToolhelp32Snapshot(), Thread32First(), Thread32Next() を使います。この API は全プロセスの全スレッドを巡回するので、該当スレッドが自身のプロセスに属するかをチェックする必要がある点に注意が必要です。
・TLS
未検証。TlsAlloc() 一族を乗っ取ればなんとかできそうな気がします。fs レジスタから Thread Information Block をたどって直接保存することでも実現できるかもしれません。
- スレッドの状態
スタックの保存についてはメモリの項で触れた通りで、それ以外の状態、レジスタの内容については、GetContext() & SetContext() するだけです。意外と簡単です。
- カーネルオブジェクトの状態
ファイルとか mutex とか socket とか。厄介な部分です。
これらに関しては汎用的な対処法がなく、個別対応が必要になります。関連する API を乗っ取って状態を追跡して復元可能にするための情報を記録。HANDLE 自身も復元可能にする必要があるため、WinAPI が返した HANDLE は直接は見せず、独自に生成&管理した復元可能な HANDLE を返して翻訳する処理を挟む必要があります。ネットワークやプロセス間通信が絡むと復元は困難を極めると予想されます…。また、DirectX の COM オブジェクトなんかも同じ対応が必要になると思われます。
https://github.com/i-saint/scribble/tree/master/RestoreProcessState
上記解説のメモリとスレッドの保存を実装して、既存プログラムへの外部からの state save & load を限定的ながら実現したのがこちら。メインスレッドのみ状態を保存、メモリはメインモジュール (exe) が扱うメモリのみ状態を保存。カーネルオブジェクトの類は未考慮。
動画は勝手に アスタブリード を実験に使わせていただいたものです。state load 後にサウンドのスレッドのストリーミング処理と思しき部分でクラッシュするという問題があり、対応が大変そうだったため、事前にデバッガでそのスレッドを止めることで強引に回避しています。真面目に対処する場合カーネルオブジェクトまで含めてきちんと面倒を見る必要がありそうです。
カーネルオブジェクトの類が未考慮なので、save -> load の間にテキスチャの破棄とかがあったら死にますし、save/load のタイミングが悪くても死にます。(mutex を lock した直後に load してしまうと dead lock が発生、など) 実用には程遠いですが、運が良ければ機能する可能性がなきにしもあらずです。
主要なカーネルオブジェクトと DirectX あたりまで対応できたら実用の域まで行けそうな気がしますが、たぶん数ヶ月を要する労力が必要な上、本当に可能かどうかも定かではないので今回はここで切り上げました。余裕がある時期が訪れたら再度真剣に検証してみたいところです。
get function by filename and line number
Alcantarea を実装するにあたって、更新する関数を最小限にするため、Visual Studio のテキストエディタの現在位置の関数を取得してその関数だけを更新する必要がありました。この ファイル名と行番号からその位置の関数を取得する という処理が無駄に奥深かったので書き残しておこうと思います。
大きく分けて 2 通りの方法があると思われます。1 つはソースファイルをパースして関数を特定する方法。もう 1 つはデバッグ情報を使ってバイナリから情報を抽出する方法です。以下は実際に検証したアプローチとそのサンプルコードです。
ちなみに、Visual Studio アドインは現在開いてるドキュメントの情報を以下のようにして取得できます。
(TextDocument が必要な情報を大体持っています。ファイルのパスしかりカーソル位置しかり)
DTE dte = (DTE)GetService(typeof(DTE)); TextDocument tdoc = dte.ActiveDocument.Object() as TextDocument;
- TextDocument.Selection.ActivePoint を使う (sample code)
アドインから Visual Studio が保持している情報を使うアプローチです。TextDocument.Selection.ActivePoint に現在位置に関連する情報が入っているのでそれを使います。最もストレートな方法で、大抵はこれで事足りると思われます。現在の Alcantarea はこの方法を用いています。
注意すべき点として、マクロで包まれてる関数とかでたまに取得できないことがあるのと、.sdf ファイル (Intellisense のデータベース) が何らかの理由で読めていない場合に例外が飛んできます。意外と万能ではないという感触です…。
また、ドキュメントに含まれるシンボル群を保持する FileCodeModel なるオブジェクトがあるのですが、こちらには罠があります。宣言が .h にあって定義が .cpp にあるシンボルは、.cpp 側の FileCodeModel には情報がありません。なぜか .h 側にだけあります。
選択範囲に含まれるシンボルを全部更新対象に加える、みたいなことを FileCodeModel に含まれる情報を巡回することで実現したかったのですが、この謎仕様により断念しました。CodeModel 関連は ここ から辿れるドキュメントが参考になります。
他には、VisualAssistX が外部にインターフェースを公開しているようなので、それを使えばより信頼性の高い情報を取得できるかもしれません (未検証)。
- dbghelp の SymEnumLines() を使う (sample code)
デバッグ情報を使うアプローチです。御存知の通り、デバッグ情報 (.pdb) には行情報が含まれています。SymEnumLines() を使うことでこの行情報を巡回できます。
簡単で信頼度も高い方法ですが、Alcantarea 実行中は実行コードは書き換えられる一方でデバッグ情報は変わらないという特殊な状況のため、編集するにつれてズレていくので使えませんでした。
- .obj のデバッグ情報を使う (sample code)
こちらもデバッグ情報を使うアプローチです。.obj ファイルにも当然デバッグ情報が含まれており、行情報もあります。このデバッグ情報をがんばって自力でパースして該当関数を見つけます。
Visual Studio 2005 以降の .obj ファイルのデバッグ情報は CV8 (Code View 8) という形式らしいのですが、これはフォーマットが非公開です。(CV4 は公開されているものの、CV8 はほとんど別物になってる様子)
しかし、誰かが独自に解析して こういうメモ を残してくれています。サンプルコードはこのメモを元に実装したもので、いい感じに機能しているように見えます。
Alcantarea は今後のリリースでこのアプローチに差し替えるかもしれません。また、このメモの情報を元にカスタムデバッガを書けば、編集後もソースコードデバッグできるようにできそうな気がするのでいずれ検証してみたいところです。
Alcantarea
C++ ソースの変更を実行中のプログラムに反映させる Visual Studio アドインをリリースしました。
Alcantarea - A Visual Studio Add-In for Runtime C++ Code Editing
これは DynamicPatcher に改良を加えつつ Visual Studio のアドイン化したもので、既存のプログラムを前準備なしに実行時編集可能にします。Edit and Continue と比べていくつか制限はあるものの、x64 でも最適化が有効なプログラムでも機能するのはゲーム開発で強力に威力を発揮するはずです。
Visual Studio 2013 が x64 の Edit and Continue に対応する、という情報が出て世界中のゲーム開発者が沸き立ち、その後実は .Net しか対応してないことが判明して世界中のゲーム開発者をガッカリの渦に巻き込んだ、という出来事がありましたが、そんな開発者たちにこのアドインが救いの手を差し伸べる…かもしれません。
このアドインを作る過程で得られた知見が色々あるので、後日書き残していこうと思います。
また、今回も告知が遅くなりましたが、冬コミ参加予定です。スペースは 3日目 (31 日) 西た-09b primitive で、予定を変更して上記アドインを 1000 円で頒布します。通常購入と同じく 1 年間のサポート&アップデートを含むライセンスキー付属です。
ここしばらく他に優先してやらないといけないことがあって atomic の開発は休眠状態です…。
それと、しばらく前に DynamicPatcher が Riot Games に採用されるという出来事がありました。
Riot Games は League of Legends のデベロッパであり、League of Legends は最大同時接続数 500 万人を超えて今世界で最も遊ばれているゲームと言われているお化けタイトルです。最も成功した Free-To-Play モデルのゲーム、として言及されることもしばしばあります。
そんなタイトルの開発を自分の黒魔術コードが支えるなんてエンジニア冥利に尽きるってもんです。
runtime member variable editing
x86 勉強会で、clang のソースコード解析機能で class の構造を得て実行時にデータを表示したり編集したりする、という話を聞いて、以前デバッグ情報を使って似たようなことをやろうとしてたのを思い出してちゃんとやってみました。
指定オブジェクトのメンバ変数を巡回して表示する例。
デバッグ情報を使って class のメンバ変数を巡回するのはこの記事やこの記事が参考になります。一見ややこしいですが、実際にやってみるとすぐ慣れます。
型情報を得るにはまず型の名前の文字列を得る必要があります。RTTI が有効であれば typeid(hoge).name() で簡単に得られますが、そうでない場合、virtual 関数を持つ class であれば vftable のシンボル名から得ることができます (実装例)。
RTTI が無効で virtual 関数もない場合は…たぶんユーザーに教えてもらうしかありません。
(追記 2013/09/27: SymEnumSymbols() とかで特定 scope の変数を巡回でき、型名も取れるため、デバッグ情報だけで typeid().name() 相当のことも可能です。実装例)
このメンバ変数巡回を WebDebugMenu に組み込んでみました。
これだけで実行時データ編集できるのはなかなか強力なんじゃないかと思います。
しかし、関数を呼ぶノードは eval の類がないと実行時自動生成は困難で、微妙に痒いところに手が届かない感もあります。可能は可能なので今後の課題です。
あとは シリアライザを自動的に生成 する、というのをやってみました。やってることは __FILE__ で自身の中身を見て特定マクロの箇所にシリアライザのコードを追記する、というものです。
他のアプローチとして、VisualC++ のコンパイラには class や struct の構造を出力するオプションがあります。(/d1reportSingleClassLayout /d1reportAllClassLayout) 型が出ないのがやや残念ですが、デバッグ情報使う以外にこういう選択肢もあります。
$ cl TestMemberNodes.cpp /d1reportSingleClassLayoutTest class Test size(80): +--- 0 | m_i32 4 | m_f32x4 20 | m_b | <alignment member> (size=11) 32 | __m128 m_m128 48 | m_charstr 64 | m_pair +---
また、gcc 系のツールチェインでは shinh さんが同じようなことをやっておられます (8 年前に)。
Cookie Clicker
https://github.com/i-saint/scribble/tree/master/CookieClicker
自分の時間を救うために Cookie Clicker の自動連打&自動 golden cookie クリックツールを作りました。
自動連打は SendInput() しまくるだけですが、golden cookie 探しは結構大変で、Cookie Clicker のウィンドウを探して、GDI 系 API でスクリーンショットを得て、前回探した時から変化した部分を探して…とややこしいことをやっています。(たまに右の building ボタンをクリックしてしまいますが、これは明るくなった時 golden cookie と誤認してしまうためです)
javascript でブラウザの内部状態を得て対処できればよりスマートですが、やり方が分からず断念しました。他の自分の時間を救おうとしてる方が別の解法を編み出してくれることを期待します。