サンプル2. DirectXランタイムが出力するデバッグメッセージのログを作成する
DirectX Graphicsデバッグランタイムや、D3DXのデバッグ版は、さまざまな情報をデバッグメッセージに出力してくれます。次のような不具合のあるコードで実験してみましょう。関数Renderでは、IDirect3DDevice9::BeginSceneで開始した描画をIDirect3DDevice9::EndSceneによって終了していないため、IDirect3DDevice9::Presentの呼び出しが失敗します。
#include <Dxerr9.h> #pragma comment (lib, "Dxerr9.lib") HRESULT Render(IDirect3DDevice9* device) { HRESULT hr = device->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 ); hr = device->BeginScene(); // 意図的にエラーを発生させる // hr = device->EndScene(); hr = device->Present( NULL, NULL, NULL, NULL ); ::OutputDebugStringA( "DXGetErrorDescription9 : " ); ::OutputDebugStringA( DXGetErrorDescription9(hr) ); ::OutputDebugStringA( "\n" ); return hr; }
このとき、デバッガのデバッグウィンドウには次のように表示されます。
Direct3D9: (ERROR) :Present not allowed between BeginScene andEndScene. Present fails. DXGetErrorDescription9 : Invalid call
上の行が「d3d9d.dll」が出力したデバッグメッセージで、下の行がIDirect3DDevice9::Presentの戻り値からDXGetErrorDescription9関数を使用して得たエラーの説明です。「d3d9d.dll」が出力したデバッグメッセージは理由を端的に説明してくれています。一方、戻り値から得たメッセージでは、失敗箇所はわかっても失敗の原因についてはほとんど何の情報も得られません。この比較からも分かるように、開発者は可能であればまず「d3d9d.dll」や「d3dx9d_28.dll」が何か有益な情報を出力していないかチェックするべきです。しかし、必ずしも開発者がテストを実行するとは限りませんから、デバッグランタイムが出力するメッセージのログを簡単に残せると便利だと考えました。
「d3d9d.dll」や「d3dx9d_28.dll」は内部でOutputDebugStringA APIを使用しているので、ログの取得には先ほどのAPIフックの仕組みがそのまま利用できます。「Sample2」はこの方法でカレントディレクトリに「dxlog.txt」というログファイルを作成し、OutputDebugStringA APIに渡された文字列を書き込んでいきます。ただし、このサンプルを実行するときはデバッグランタイムを有効にしておいてください。リテールランタイムは高速に動作する代わりに、何もデバッグメッセージを出力しません。
例として「kernel32.dll」が公開する
LoadLibraryA APIにブレークポイントを仕掛けてみましょう。[Ctrl]+[B]キーを押して、「ブレークポイントの作成」ダイアログボックスを表示します。32bitモードで実行していて、「kernel32.dll」のシンボルファイルが既に読み込まれているときは、次のように設定します。
{,,kernel32.dll}_LoadLibraryA@4
{,,kernel32.dll}LoadLibraryA
次にVisual C++ 2005の新機能であるトレースポイントを使用してみましょう。ブレークポイント一覧から先ほど作成したブレークポイントを右クリックし、[ヒット時]を選択します。
![図11 [ヒット時]を選択 図11 [ヒット時]を選択](http://cz-cdn.shoeisha.jp/static/images/article/235/tracepoint1.gif)
| 32bit実行時 | LoadLibraryA( {*((char **)(@esp+4)),s} ) $CALLSTACK |
| 64bit実行時 | LoadLibraryA( {(char **)(@rcx),s} ) $CALLSTACK |
__stdcallの指定を全て無視し、必ず新しい呼び出し規約を使用します。さっそく「Sample1」で作成したプログラムに、このトレースポイントを適用してみましょう。すると Visual C++ 2005の[出力]ペインに、このようなログが出力されます。
LoadLibraryA( "d3d9.dll" ) kernel32.dll!_LoadLibraryA@4()
d3dx9_28.dll!0052ac04()
[Frames below may be incorrect and/or missing,
no symbols loaded for d3dx9_28.dll]
d3dx9_28.dll!00552d55()
d3d9d.dll!04a7c7ff()
d3d9d.dll!04a7c7ff()
d3d9d.dll!04a7c7ff()
d3d9d.dll!04a7c7ff()
user32.dll!_ReleaseDC@8()
d3d9d.dll!04a7c7ff()
uxtheme.dll!CThemeWnd::NcPaint()
user32.dll!_IsWindow@4()
デバッグランタイムにフックを仕掛ける上で注意すべき点としては、「d3d9d.dll」にフックを仕掛けるタイミングと、D3DXライブラリがどのようにリンクされるかという2点です。
まず「d3d9d.dll」から片付けましょう。「HKLM\Software\Microsoft\Direct3D\LoadDebugRuntime」に1がセットされているとき、「d3d9.dll」は内部でLoadLibraryA APIを使用して「d3d9d.dll」をロードします。つまりプロセス起動直後には、「d3d9d.dll」はロードされていないことがほとんどです。「d3d9d.dll」のロード直後からロギングを有効にしたかったので、「d3d9.dll」のLoadLibraryA APIもフックしてしまうことにしました。こうすればロード直後の「d3d9d.dll」に新しいフックを仕掛けることができます。「hookmain.cpp」のInitHook関数から該当部分を抜き出してみましょう。
// D3D をフック if( FAILED( Hook( GetModuleHandleA( "d3d9.dll" ), "Kernel32.dll", "LoadLibraryA", (PROC) D3DHookLoadLibraryA, (PROC*) &g_d3d_orgLoadLibraryA ) ) ) { MessageBoxA( NULL, "d3d9.dll のフックに失敗しました", "Sample 2", MB_ICONERROR | MB_OK ); return 1; }
g_d3d_orgLoadLibraryAは、「d3d9.dll」で本来LoadLibraryA APIを指していた関数ポインタで、D3DHookLoadLibraryAは置き換えられるフックハンドラです。D3DHookLoadLibraryAが呼び出されると、次のようにOutputDebugStringAの置き換えを行います。これで「d3d9d.dll」のフックは完了です。
HMODULE WINAPI D3DHookLoadLibraryA( LPCSTR dllName )
{
HMODULE module = g_d3d_orgLoadLibraryA( dllName );
if( _stricmp( dllName, "d3d9d.dll" ) == 0 )
{
// OutputDebugStringA をフックする
Hook( module,
"Kernel32.dll",
"OutputDebugStringA",
(PROC) MyOutputDebugStringA,
(PROC*) &g_d3d_orgOutputDebugStringA );
}
return module;
}
今度はD3DXの番です。D3DXには歴史的にさまざまな提供方法があり、アプリケーションとどのようにリンクしているかによって、フックすべきモジュールが異なります。大きく分ければ、D3DXがEXEファイルに静的リンクされている場合と、D3DX DLLとしてアプリケーションに動的リンクしている場合の2通りを考える必要があります。もちろん、アプリケーション製作者はリンカに好きな指示を与えることができますから、以下の判定は特定のアプリケーション用にハードコードしてしまっても構いません。
表4はD3DXをどのようにリンクできるかまとめたものです。DirectX SDKのバージョンによってとり得る形態が異なります。
| DirectX 9.0 SDKから DirectX 9.0 SDK Update - (December 2004)まで | 静的リンク、または「d3dx9d.dll」と動的リンク |
| DirectX 9.0 SDK Update - (February 2005)以降 | 「d3dx9_XX.dll」または「d3dx9d_XX.dll」と動的リンク (ただしXXはSDKバージョン) |
「d3dx9d.dll」と「d3dx9d_XX.dll」は再配布可能ファイルから除外されています。そのため、エンドユーザ環境で「d3dx9d.dll」に関するDLL Hellが発生することは通常考えられません。
D3DXが静的リンクされている場合は、現在の実行ファイル(つまりEXEファイル)に対してフックを行うことになります。ただしこの場合、D3DXが呼び出すOutputDebugStringAだけでなく、実行ファイル中の全てのコードが使用するOutputDebugStringAも同様にフックされてしまいます。これは思わぬ副作用をもたらす可能性があるため、サンプルコードではD3DXが静的リンクされていると判断した場合はOutputDebugStringA APIのフックを行いません。
void AddLog( const char* string ) { if( g_logFile == NULL ) { ::OutputDebugStringA( "AddLog failed: g_logFile is NULL!\n" ); return; } WriteFile( g_logFile, lpOutputString, srcLen, &len, NULL ); } void WINAPI MyOutputDebugStringA( LPCSTR lpOutputString ) { AddLog( lpOutputString ); } int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { if( Hook( GetModuleHandleA(NULL), "Kernel32.dll", "OutputDebugStringA", (PROC) MyOutputDebugStringA, NULL ) ) { AddLog( "Hook OK!\n" ); }
D3DXがDLLとしてリンクされているかどうかはGetModuleHandleA APIの結果で判断しています。ただしこの方法でうまく検出できるためには、D3DX DLLが既にプロセスへロードされている必要があります。例えばD3DX DLLを遅延ロードさせている場合には、判定に失敗することがあります。このコードはコンパイル時のD3DX_SDK_VERSIONに対応するモジュールのみを探しますが、もっと汎用化して、例えばexeファイルのインポートセクションからD3DXらしきDLLを探すようにしてもよいでしょう。
#define D3DX_SDK_VERSION_FEB2005 24 HMODULE d3dxModule = NULL; #if D3DX_SDK_VERSION >= D3DX_SDK_VERSION_FEB2005 char dll1[MAX_PATH]; char dll2[MAX_PATH]; sprintf( dll1, "d3dx9_%d.dll", D3DX_SDK_VERSION ); sprintf( dll2, "d3dx9d_%d.dll", D3DX_SDK_VERSION ); // D3DX DLL が遅延ロード指定を受けている場合には判定に失敗する d3dxModule = GetModuleHandleA( dll1 ); if( d3dxModule == NULL ) { d3dxModule = GetModuleHandleA( dll2 ); } #else // 従来も 「d3dx9d.lib」とリンクした場合は // デバッグ用の DLL が使用されていた const char* dll3 = "d3dx9d.dll"; if( d3dxModule == NULL ) { d3dxModule = GetModuleHandleA( dll3 ); } #endif if( d3dxModule == NULL ) { // D3DX は(恐らく)静的リンクされている //d3dxModule = GetModuleHandleA( NULL ); } if( d3dxModule == NULL || FAILED( Hook( d3dxModule, "Kernel32.dll", "LoadLibraryA", (PROC) MyOutputDebugStringA, (PROC*) &g_d3dx_orgOutputDebugStringA ) ) ) { MessageBoxA( NULL, "d3dx9.dll のフックに失敗しました", "Sample 2", MB_ICONERROR | MB_OK ); return 1; } #undef D3DX_SDK_VERSION_FEB2005
「d3d9.dll」が存在しない理由のほとんどは、エンドユーザの環境にDirectXランタイムがインストールされていないことです。しかし残念なことに、開発者がそのことを伝える前にOSのローダーは上記のような、一般的で正確な、しかし不親切なメッセージを表示してしまいます。そしてエンドユーザは説明書を見る前に、Webブラウザを立ち上げ検索ページに向かうことでしょう。
このようなダイアログの代わりに、ユーザに最新のDirectXランタイム導入を促すメッセージを伝えられれば、多くの人々の大切な時間を節約できるに違いありません。その方法のひとつは、「d3d9.dll」を静的インポートせずに、実行時に動的にロードするというものです。
LoadLibraryA APIの呼び出しに失敗しても直ちにプロセスがクラッシュするということはありませんから、アプリケーションはもっと穏便なメッセージを表示させてからプロセスを終了することができます。幸い、ほとんどの開発者は
Direct3DCreate9以外のエクスポート関数に用はありませんので、GetProcAddressA APIの呼び出しは1回だけで済みます。この方法は実際、最近のDirectX SDKでソースコードとして提供されているDXUTフレームワークで用いられています。DXUTはプロセス起動後にLoadLibraryA APIを用いて「d3d9.dll」のロードを試み、失敗した場合は次のようなメッセージを表示します。GetProcAddressA APIで実行時にバインドすることは手間ですし、何よりこういった定型的な作業は自動化されるべきです。実際もう何年も前から、具体的にはVisual C++ 6.0以降、これらの作業を自動化するDLLの遅延ロード機能がサポートされています(参考資料5)。「Column8」フォルダの「CheckD3DX.exe」サンプルプログラムは、DLLの遅延ロードを利用して「d3dx9_28.dll」が存在するかどうかを確認します。このプログラムは「d3dx9_28.dll」が見つかったかどうかで次のように表示を変えます。


LoadLibraryA APIやGetProcAddressA APIの代わりに、ユーザの指定した値を遅延ロードライブラリに使用させることが可能です。この方法を用いれば簡単にAPIフックを実現できるでしょう。詳細については遅延ロードの「通知フック」(参考資料5)を参考にしてください。なお、今回のサンプルプログラムはどれもDirectX DLLが公開するAPIをフックしませんので、DirectX DLLを遅延ロードしても基本的に問題はありません。気をつける必要があるのは、DLLを遅延ロード指定した場合、プロセス起動直後に
GetModuleHandleA APIを呼び出してもNULLが返るようになったかもしれない、ということです。 最後にログ出力ルーチンについて見ておきましょう。ログの出力はD3DHookOutputDebugStringAとD3DXHookOutputDebugStringAの中で行っています。異なるハンドラ使用しているのは、呼出し元を区別するためです。もちろん、特に区別する必要がなければ、同じハンドラにまとめてしまって構いません。サンプルでは、このフックハンドラはプロセス終了まで設定されたままとなります。
// ログに記録するための OutputDebugStringAのフックハンドラ // 「d3d9d.dll」 から呼び出される void WINAPI D3DHookOutputDebugStringA( LPCSTR lpOutputString ) { if( g_d3d_orgOutputDebugStringA != NULL ) { g_d3d_orgOutputDebugStringA( lpOutputString ); } if( g_logFile != NULL ) { size_t srcLen; if( FAILED( StringCbLength( lpOutputString, 65535, &srcLen ) ) ) { return; } DWORD len = NULL; WriteFile( g_logFile, lpOutputString, srcLen, &len, NULL ); } } // ログに記録するための OutputDebugStringA のフックハンドラ // D3DX から呼び出される void WINAPI D3DXHookOutputDebugStringA( LPCSTR lpOutputString ) { if( g_d3dx_orgOutputDebugStringA != NULL ) { g_d3dx_orgOutputDebugStringA( lpOutputString ); } if( g_logFile != NULL ) { size_t srcLen; if( FAILED( StringCbLength( lpOutputString, 65535, &srcLen ) ) ) { return; } DWORD len = NULL; WriteFile( g_logFile, lpOutputString, srcLen, &len, NULL ); } }
DLL_PROCESS_DETACHを受け取るタイミングでは、一般にEXEファイルに静的リンクしたCRTの開放処理は終了しています。もし、あるDLLがこのタイミングで何らかのAPIを呼び出し、さらにそのAPIがフックされている場合はどうなるでしょうか?「d3d9d.dll」は
DLL_PROCESS_DETACHを受け取ると未開放のDirectX Graphicsオブジェクトが残っていないかチェックします。このときランタイムはOutputDebugStringA APIを利用してメッセージを出力します。これはつまり、「d3d9d.dll」が呼び出すOutputDebugStringAのフックをそのままにしておくのなら、フックハンドラはアプリケーション終了時にもクラッシュすることなく実行できなければならないということです。図15は実際このルールを守らなかったためにクラッシュしてしまったテストコードのコールスタックです。このテストコードでは「d3d9d.dll」が呼び出した
OutputDebugStringA APIのフックハンドラ内で、既にデストラクタが呼ばれた後のグローバル変数にアクセスしていました。実際のところDLLのエントリポイントが呼ばれたときに「kernel32.dll」以外のDLLが有効である(すなわち、既にプロセス空間にロードされて、初期化処理が完了している)という保障はありません(参考資料5)。つまり、DLLのエントリポイントで使用できるAPIは、最初からかなり限定されてしまっているのです。また、厄介な循環の問題を回避するために、エントリポイント内で他のDLLをロードまたはアンロードすることも禁止されています(参考資料9)。安全を期すのであれば、プロセス終了時に最後まで使用するフックハンドラは、「kernel32.dll」が提供するAPIのみで記述したほうが良いでしょう。とはいえ、世の中には有益だがルールを守らないプログラムがたくさん存在しますので、実際のNT Loaderは、とても慎重にDLLの開放順序を決めています(参考資料10)。

EndScene. Present fails.
DXGetErrorDescription9 : Invalid call


