サンプル1. アプリケーション起動時にRetail/Debug Runtimeを選択する
DirectXにはエンドユーザ向けのRetail Runtimeの他に、開発者向けのDebug Runtimeも提供されています。通常のDirectX再配布ファイルではDebug Runtimeはインストールされません。Debug Runtimeの動作には次のような違いがあります。
- 実行時により入念なパラメータ検証が行われるようになる。
- 開発に役立つさまざまな情報を
OutputDebugString
APIでデバッガに出力してくれる。
開発者向けのDirectXランタイムをインストールすると、コントロールパネルにDirectXというアプレットが追加され、ランタイムの設定を行えるようになります。
残念ながら、この設定アプレットと同等の設定を行う専用APIは用意されていません。この章では、デバッグランタイムとリテールランタイムのどちらを使用するか、プログラムから制御する方法について検討を行います。
RegMonとFileMonでモニタリングしてみると、設定情報は主にレジストリと「%windir%\win.ini」に格納されていることが分かります。図8は、RegMonによってモニタリングしている様子を示しています。
表2は、Direct3Dタブで設定できるパラメータの設定場所を示しています。
表示項目 | データ格納場所 |
Debug/Retail D3D Runtime | HKLM\Software\Microsoft\Direct3D\LoadDebugRuntime |
Enable Shader Debugging | HKLM\Software\Microsoft\Direct3D\EnableDebugging |
Maximum Validation | HKLM\Software\Microsoft\Direct3D\FullDebug |
Break on Memory Leaks | HKLM\Software\Microsoft\Direct3D\BreakOnMemLeak |
Break on D3D Error | HKLM\Software\Microsoft\Direct3D\BreakOnDPF |
Enable Multi-mon Debugging | HKLM\Software\Microsoft\Direct3D\EnableMultimonDebugging |
Allow Hardware Acceleration | HKLM\SOFTWARE\Microsoft\Direct3D\Drivers\SoftwareOnly |
Enumerate Ramp Rasterizer | HKLM\SOFTWARE\Microsoft\Direct3D\Drivers\EnumRamp |
Enumerate Reference Rasterizer | HKLM\SOFTWARE\Microsoft\Direct3D\Drivers\EnumReference |
Debug Output Level | win.ini [Direct3D] debug=x (x = 0 - 4) |
設定の格納場所が分かったことで、アプリケーション起動直前に「HKLM\Software\Microsoft\Direct3D\LoadDebugRuntime」 を変更することにより、使用するランタイムを変更できるようになりました。しかし、レジストリの書き換えには権限が必要なことと、書き換えによってシステム全体に影響が及ぶことを考慮し、今回はよく知られたAPIフック (参考資料5)の手法を用いることにします。DirectXが実際にレジストリにアクセスする前にAPI呼出しを横取りすることで、レジストリに設定する必要を無くしてしまおうというわけです。
WindowsでのAPIフックにはいくつか方法が知られていますが(参考資料5、6)、今回は自プロセスのアドレス空間にあるインポートアドレステーブル(IAT)を書き換える方法を採用しています。この方法であれば影響範囲は自分自身のプロセスに限定されます。
実際にフックを行うHook
関数は「apihook.h」と「apihook.cpp」で実装されています。この関数は、x64アプリケーション内でも正しく動くように作ってあります。
HRESULT Hook( HMODULE module, const char* dllName, const char* procName, PROC newProc, PROC* originalProc);
Hook
関数は、dllName
で指定されたDLLが提供するprocName
という名前のエクスポート関数の呼び出しを、実際にはnewProc
という関数が呼び出されるように置き換えます。ただし置き換えの影響はmodule
で指定されたモジュール内に限定されます。例えば「d3d9.dll」の内部で呼び出されるRegQueryValueExA
APIの呼び出しをD3DHookRegQueryValueExA
関数で置き換えたい場合、次のように使用します。
static RegQueryValueExAEntry g_d3d_orgRegQueryValueExA = NULL; bool InitHook() { if( FAILED( Hook( GetModuleHandleA( "d3d9.dll" ), "Advapi32.dll", "RegQueryValueExA", (PROC) D3DHookRegQueryValueExA, (PROC*) &g_d3d_orgRegQueryValueExA ) ) ) { MessageBoxA( NULL, "d3d9.dll のフックに失敗しました", "Sample 1", MB_ICONERROR | MB_OK ); return false; } return true; }
筆者の環境で試したところ、「d3d9.dll」はDirect3DCreate9
が呼び出された直後に、レジストリから「HKLM\Software\Microsoft\Direct3D\LoadDebugRuntime」の値を読みとっていました。このとき0が返ればRetail Runtimeが、1が返ればDebug Runtimeがそれぞれ使用されています。「Sample1」の「hookmain.cpp」で実装されているD3DHookRegQueryValueExA
関数は、このタイミングでデバッグランタイムを使用するかどうか問い合わせるためのフックハンドラです。
LONG WINAPI D3DHookRegQueryValueExA( HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData ) { // lpValueName が "LoadDebugRuntime" であるものを処理する。 // 本来は hKey が HKEY_LOCAL_MACHINE\Software\Microsoft\Direct3D // を指していることを確認すべきである。 if( _stricmp(lpValueName, "LoadDebugRuntime") == 0 ) { // 実際にはサイズの問い合わせやエラーとなる呼び出しについても // シミュレートする必要がある。 if( lpcbData != NULL && *lpcbData == 4 ) { if( lpData != NULL ) { const int result = MessageBoxA( NULL, "デバッグランタイムを使用しますか?", "Sample 1", MB_YESNO ); *reinterpret_cast<DWORD*>(lpData) = ( result == IDYES ? TRUE : FALSE ); } if( lpType != NULL ) { *lpType = REG_DWORD; } return ERROR_SUCCESS; } } // 通常は元の RegQueryValueExA に処理を委譲する return g_d3d_orgRegQueryValueExA( hKey, lpValueName, lpReserved, lpType, lpData, lpcbData ); }
なお、「LoadDebugRuntime」に0が格納されていることを完全にエミュレートするためには、RegQueryValueExA
のさまざまな引数のパターンと、RegGetValue
やRegEnumValue
などのAPI、そして各APIのUnicodeバージョンも考慮する必要があります。
またこのフックハンドラは、hKey
レジストリキーハンドルを確認しないという問題があります。そのため、このハンドラはレジストリキーの場所に関係なく、「LoadDebugRuntime」という値に対するアクセス全てを横取りしてしまいます。hKey
がどのレジストリキーを指しているか取得するAPIがあればよいのですが、探した限りでは見つけることができませんでした。考えられる対策は、レジストリキーを取得できるAPI全てをフックして有効なハンドルを追跡することです。
デバッグランタイム以外の多くの設定についても、レジストリAPIのフックで対応できますが、唯一"Debug Output Level"の設定については、「win.ini」ファイルへのアクセスを横取りする必要があります。
Interlocked
APIの使用を検討してください。Visual C++のC Run-Time Libraries(CRT)やStandard C++ Libraryについても正しく理解することが必要です。マルチスレッド用のCRTは、スレッドごとにワーク領域を確保します。この領域には、スレッドごとに異なる必要があるさまざまな情報が格納されており、例えば
rand
関数の状態管理やstrerror
関数が返す文字列の保持などに使用されています。具体的な内容についてはCRTソースコードの_tiddata
(_ptiddata
)構造体で確認することができます。CRTはスレッドローカルストレージ(TLS)を利用して、実行中のスレッドに関連付けられた管理ブロックを特定します。TLSには、(存在するならば)そのスレッド用の管理ブロックのメモリアドレスが格納されています。もしアドレスが格納されていなければ、そのスレッド用の管理ブロックはまだ作成されていないことを意味します。CRTは、管理ブロックが必要になったときに初めて管理ブロックを作成し、そのメモリアドレスをTLSに格納します。
OSはスレッド終了時に、TLSの使用するメモリ領域を適切に開放します。しかしCRTが使用する管理ブロックは、そのメモリアドレスのみがTLSに格納されているため、管理ブロック自体は自動的には開放されません。CRTはスレッド終了時に、管理ブロックのアドレスを(存在するならば)読み出して開放処理を行う必要があります。この処理が実行されなかった場合、メモリリークが発生します。
CRTはいくつかの方法で、スレッドの終了に開放処理を割り込ませています。ひとつの方法は、DLLのエントリポイントを利用する方法です。Windows OSは、いくつかのイベントが発生するとプロセス中のDLLエントリポイントを順番に呼び出します。スレッドの終了もこのイベントのひとつで、このとき
fdwReason
引数にDLL_THREAD_DETACH
が格納されます。DLLとしてリンクされるCRTは、この通知を利用して管理ブロックの開放を行います。CRTが静的リンクされる場合であっても、生成物がDLLである場合は同じ方法が使用できます。Visual C++のDLLプロジェクトでは、
__DllMainCRTStartup
がエントリポイントに指定されることで、開発者の記述したDllMain
の前後に必要な処理を割り込ませています。生成物がexeファイルで、かつCRTが静的にリンクされる場合は、実際のところ非常に困難な問題が発生します。スレッドを作成するのが開発者自身であれば、
CreateThread
APIの代わりに「process.h」で宣言されている_beginthreadex
関数を使用します。_beginthreadex
関数は、引数に指定されたプロシージャの実行前と実行後にCRTの処理を実行することができます。しかし、別のモジュールによってスレッドが作成される場合にはこの方法が使用できません。あるDLLが内部で呼び出す_beginthreadex
関数は、exeファイルと静的にリンクしたCRTの_beginthreadex
関数とは異なるからです。Windows Server 2003以降(これにはWindows XP Professional x64 Editionも含まれる)のOSでは、
FlsAlloc
を使用することで、ファイバ終了時にOSからコールバックを受けることができるようになりました。ファイバローカルストレージ(FLS)は、ファイバを全く使用しない場合でもTLSの代用として使用できます。そのためVisual C++ .NET 2003のCRTやVisual C++ 2005のCRTは、可能であればファイバローカルストレージを利用することで、確実に管理ブロックを開放するようになっています。従来のTLSにもTLS Callbackと呼ばれるコールバックメカニズムが存在していましたが、Portable Executable(PE)ファイルヘッダにコールバック関数を定義する必要があることや、Windows 9xではこの機能が実装されていないこともあって、必ずしも使い勝手の良いものではありませんでした参考資料19。TLS Callbackによるリーク回避の実装については、boost MLの『Windows MSVC thread exit handler for staticly linked Boost.Thread』というスレッドでの議論が参考になります。
_beginthreadex
関数を用いても実際にリークが発生しうることを示すのがサンプルプログラム「MTCRT」です。「MTCRT」サンプルは、スレッドを作成し受け取った関数ポインタ実行する「Callback.dll」と、それを呼び出すexeファイルから構成されています。exeファイルはリンクするCRTの違いで2種類存在します。- 「release_staticcrt-win32\Test.exe」 静的リンクCRT使用版(32bit版)
- 「release_dynamiccrt-win32\Test.exe」 動的リンクCRT使用版(32bit版)
- 「release_staticcrt-x64\Test.exe」 静的リンクCRT使用版(64bit版)
- 「release_dynamiccrt-x64\Test.exe」 動的リンクCRT使用版(64bit版)
DoWork
が実装されています。CALLBACK_API HANDLE DoWork(WorkProc workProc) { return (HANDLE) _beginthreadex( NULL, 0, workProc, NULL, 0, NULL ); }
DoWork
を呼び出します。exeファイルで実装されたWork
関数は、DoWork
の中で作られたスレッド上でコールバック実行されます。Work
関数の中でスレッド管理ブロックを必要とするCRT関数を呼び出していることに注意してください。Windows XP以前の環境でCRTと静的にリンクした「Test.exe」を実行すると、Work
関数で動的に確保された管理ブロックがリークします。unsigned WINAPI Work(void * arg) { strerror(1); time_t t; time( &t ); asctime( localtime( &t ) ); return 0; } int main() { for( int i = 0; i < 1000000; ++i ) { HANDLE threadHandle = DoWork(Work); if( WAIT_OBJECT_0 != WaitForSingleObject( threadHandle, 10000 ) ) { break; } CloseHandle( threadHandle ); } return 0; }
デバイスの作成が終わったら、フックを元に戻しておきます。これはオリジナルの関数ポインタを書き戻すだけです。これで「Sample1」は完成です。
bool FreeHook() { if( FAILED( Hook( GetModuleHandleA( "d3d9.dll" ), "Advapi32.dll", "RegQueryValueExA", (PROC) g_d3d_orgRegQueryValueExA, NULL ) ) ) { MessageBoxA( NULL, "d3d9.dll のフック解放に失敗しました", "Sample 1", MB_ICONERROR | MB_OK ); return false; } return true; }
GetProcAddress
APIでエクスポート関数を取得し、これを呼び出すというものです。しかし、GetProcAddress
APIを使用してもOSはDLLの参照カウントを増やしませんから、MyFuncを実行中に別のスレッドが「test.dll」をアンロードすることは原理的に可能です。これを回避するためには、呼び出し元でLoadLibrary
APIを使用して「test.dll」の参照カウントを増やしておく必要があります。HMODULE module = GetModuleHandleA( "test.dll" ); PROC proc = (module != NULL ? (PROC)GetProcAddress( module, "MyFunc" ) : NULL); if( proc != NULL ) { proc(); }
常駐アプリケーションなどによって他人のフックコードが紛れ込んでくることもあります。IATの書き換えがほぼ同時に行われた場合、IATのページ属性の変更に競合が発生するかもしれません。
無事にIATの書き換えが行われたとしても、書き換える順序は依然として問題になりがちです。実際に呼び出されるのは後から書き込んだ方のハンドラです。我々が先にフックを仕掛けていたとすれば、後から来てフック行おうとするルーチンにとって、我々のフックハンドラは「オリジナルの関数アドレス」となります。このことが正確なアンフックを難しくします。例えば以下の順序でフック解除を行うと、最終的にはAさんのフックハンドラが残ってしまいます。
- Aさんがフックを作成
- Bさんがフックを作成
- Aさんがフックを解除
- Bさんがフックを解除
SetWindowLong
APIを利用したウィンドウのサブクラス化についても同じことが言えます。信頼性の高いフック機構を設計するために、SetWindowsHookEx
APIやCallNextHookEx
APIなどの実装と、一度比較してみるとよいでしょう。これらのAPIではフックの寿命はハンドルによって管理され、フックの挿入と削除は必ず関数を介して行います。