SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

DirectX Graphicsの隠し設定を利用した開発テクニック

APIフックとレジストリ操作によるDirectXのデバッギング技法


サンプル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 and
 EndScene. 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に渡された文字列を書き込んでいきます。ただし、このサンプルを実行するときはデバッグランタイムを有効にしておいてください。リテールランタイムは高速に動作する代わりに、何もデバッグメッセージを出力しません。

コラム6 APIにブレークポイントを設定する
 アプリケーションに組み込んでどこでも利用するには、確かにAPIフックは便利な方法です。しかし、ちょっとした思い付きからあるAPIの呼出しをモニタリングしてみたくなったときのために、慣れ親しんだVisual C++のデバッガの中で作業を終わらせたいこともあるでしょう。そこでVisual C++のデバッガを用いて、APIの呼出しにブレークポイントを仕掛ける方法を紹介しておきます。
 例として「kernel32.dll」が公開するLoadLibraryA APIにブレークポイントを仕掛けてみましょう。[Ctrl]+[B]キーを押して、「ブレークポイントの作成」ダイアログボックスを表示します。32bitモードで実行していて、「kernel32.dll」のシンボルファイルが既に読み込まれているときは、次のように設定します。
{,,kernel32.dll}_LoadLibraryA@4
図10 「ブレークポイントの作成」ダイアログボックス
図10 「ブレークポイントの作成」ダイアログボックス
 64bitモードで実行しているとき、または32bitモードで「kernel32.dll」のシンボルが読み込まれていないときは、代わりに次のような文字列を使います。
{,,kernel32.dll}LoadLibraryA
 新しく作成したブレークポイントが一覧に追加され、左端に見慣れた赤く塗りつぶされた丸印が点灯したら、後は普段使用しているブレークポイントと同じです。いつもと同じように条件付きブレークポイントやフィルタを設定することができます。もし赤丸が点灯しなければ、上の文字列が正しく設定されているか、もう一度確かめてみてください。
 次にVisual C++ 2005の新機能であるトレースポイントを使用してみましょう。ブレークポイント一覧から先ほど作成したブレークポイントを右クリックし、[ヒット時]を選択します。
図11 [ヒット時]を選択
図11 [ヒット時]を選択
 「ブレークポイントのヒット時」ダイアログボックスが表示されますので、次のように入力してください。
表3
32bit実行時LoadLibraryA( {*((char **)(@esp+4)),s} ) $CALLSTACK
64bit実行時LoadLibraryA( {(char **)(@rcx),s} ) $CALLSTACK
図12 「ブレークポイントのヒット時」ダイアログボックス(入力は32bit実行モード用)
図12 「ブレークポイントのヒット時」ダイアログボックス(入力は32bit実行モード用)
 32bitモードと64bitモードで記述が異なるのは、この間にAPIの呼び出し規約が変更されたためです(参考資料7)。Visual C++ 2005のコンパイラは、x64用のバイナリを作成するにあたって__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()
 こうしてVisual Studioのデバッガによって簡単にコールスタックが得られるようになりました。後は放っておくだけで情報が手に入ります。またトレースポイントはVisual Studioのマクロを実行することもできますから、最初からこの機能をあてにしてテストベンチを作ってしまうのも、あながち悪くない考えと言えるでしょう。

 デバッグランタイムにフックを仕掛ける上で注意すべき点としては、「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関数から該当部分を抜き出してみましょう。

「d3d9.dll」のLoadLibraryAをフックする
// 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」のフックは完了です。

D3DHookLoadLibraryAの実装
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のバージョンによってとり得る形態が異なります。

表4
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のフックを行いません。

コラム7 フックハンドラと再入・再帰
 シングルスレッド環境であっても、フックを行ったために予想外の再入や再帰が発生し、プログラムがクラッシュしてしまうことがあります。例えば以下のコードは、条件により無限に再帰してしまいスタックオーバーフローが発生します。
フックハンドラの再帰
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を探すようにしてもよいでしょう。

D3DXが存在するモジュールに対してフックを行う
#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
コラム8 DirectX DLLを遅延ロードする
 DirectX Graphicsを使用したアプリケーションを配布していると、しばしば次のような質問を受けます。
 『プログラムを実行したところ「d3d9.dllが見つからなかったため、このアプリケーションを開始できませんでした。 アプリケーションをインストールしなおすと問題は解決される場合があります。」と表示されました。どうすればいいでしょうか?』
 DirectX Graphicsサンプルの多くは、「d3d9.lib」や「d3dx9.lib」インポートライブラリとリンクすることで、DirectX Graphics DLLのロードをOSに任せています。プロセスを起動するにあたってローダーは、exeファイルのPEヘッダーから静的インポートされているDLLを調べ上げ、どの順序でDLLを初期化するかを決定します(参考資料8)。この途中で必要な「d3d9.dll」 が見つからないと、上のようなエラーが表示されます。
 「d3d9.dll」が存在しない理由のほとんどは、エンドユーザの環境にDirectXランタイムがインストールされていないことです。しかし残念なことに、開発者がそのことを伝える前にOSのローダーは上記のような、一般的で正確な、しかし不親切なメッセージを表示してしまいます。そしてエンドユーザは説明書を見る前に、Webブラウザを立ち上げ検索ページに向かうことでしょう。
 このようなダイアログの代わりに、ユーザに最新のDirectXランタイム導入を促すメッセージを伝えられれば、多くの人々の大切な時間を節約できるに違いありません。その方法のひとつは、「d3d9.dll」を静的インポートせずに、実行時に動的にロードするというものです。LoadLibraryA APIの呼び出しに失敗しても直ちにプロセスがクラッシュするということはありませんから、アプリケーションはもっと穏便なメッセージを表示させてからプロセスを終了することができます。
 幸い、ほとんどの開発者はDirect3DCreate9以外のエクスポート関数に用はありませんので、GetProcAddressA APIの呼び出しは1回だけで済みます。この方法は実際、最近のDirectX SDKでソースコードとして提供されているDXUTフレームワークで用いられています。DXUTはプロセス起動後にLoadLibraryA APIを用いて「d3d9.dll」のロードを試み、失敗した場合は次のようなメッセージを表示します。
 Could not initialize Direct3D. You may want to check that the latest version of DirectX is correctly installed on your system. Also make sure that this program was compiled with header files that match the installed DirectX DLLs.
 ローカライズの必要があることを除けば、先ほどOSがDLLを見つけられなかったときに比べて、ずいぶん建設的なメッセージです。不幸だったのは、DXUTはD3DXに依存しているため「d3dx9_28.dll」を使わざるを得ないこと、そして依然として多くのサンプルプロジェクトでは「d3dx9.lib」によってD3DX DLLへの静的インポートを続けているということでしょう。DirectX 9.0cコアランタイムが利用できない環境で、D3DX DLLが見つかる可能性はまずありません。つまり、ほとんど確実に結果はこうなります。
 d3dx9_28.dllが見つからなかったため、このアプリケーションを開始できませんでした。アプリケーションをインストールしなおすと問題は解決される場合があります。
 さて、「d3dx9_28.dll」がエクスポートしている関数全てをGetProcAddressA APIで実行時にバインドすることは手間ですし、何よりこういった定型的な作業は自動化されるべきです。実際もう何年も前から、具体的にはVisual C++ 6.0以降、これらの作業を自動化するDLLの遅延ロード機能がサポートされています(参考資料5)。
 「Column8」フォルダの「CheckD3DX.exe」サンプルプログラムは、DLLの遅延ロードを利用して「d3dx9_28.dll」が存在するかどうかを確認します。このプログラムは「d3dx9_28.dll」が見つかったかどうかで次のように表示を変えます。
図13 「d3dx9_28.dll」が見つかったとき
図13 「d3dx9_28.dll」が見つかったとき
図14 「d3dx9_28.dll」が見つからなかったとき
図14 「d3dx9_28.dll」が見つからなかったとき
 遅延ロードは特殊なインポートセクションを作成するため、最初のサンプルで使用したフックルーチンは、Visual C++の遅延ロードによってインポートされるAPIをフックできないことに注意してください。ただし、遅延ロード機構は非常に柔軟な設計になっており、OS標準のLoadLibraryA APIやGetProcAddressA APIの代わりに、ユーザの指定した値を遅延ロードライブラリに使用させることが可能です。この方法を用いれば簡単にAPIフックを実現できるでしょう。詳細については遅延ロードの「通知フック」(参考資料5)を参考にしてください。
 なお、今回のサンプルプログラムはどれもDirectX DLLが公開するAPIをフックしませんので、DirectX DLLを遅延ロードしても基本的に問題はありません。気をつける必要があるのは、DLLを遅延ロード指定した場合、プロセス起動直後にGetModuleHandleA APIを呼び出してもNULLが返るようになったかもしれない、ということです。

 最後にログ出力ルーチンについて見ておきましょう。ログの出力はD3DHookOutputDebugStringAD3DXHookOutputDebugStringAの中で行っています。異なるハンドラ使用しているのは、呼出し元を区別するためです。もちろん、特に区別する必要がなければ、同じハンドラにまとめてしまって構いません。サンプルでは、このフックハンドラはプロセス終了まで設定されたままとなります。

ログ出力ルーチン
// ログに記録するための 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 );
    }
}
コラム9 プロセス終了処理とDLL_PROCESS_DETACH
 プロセスを終了する際、プロセス中の各DLLが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のフックハンドラ内で、既にデストラクタが呼ばれた後のグローバル変数にアクセスしていました。
図15 例外発生時のコールスタック
図15 例外発生時のコールスタック
 実際のところDLLのエントリポイントが呼ばれたときに「kernel32.dll」以外のDLLが有効である(すなわち、既にプロセス空間にロードされて、初期化処理が完了している)という保障はありません(参考資料5)。つまり、DLLのエントリポイントで使用できるAPIは、最初からかなり限定されてしまっているのです。また、厄介な循環の問題を回避するために、エントリポイント内で他のDLLをロードまたはアンロードすることも禁止されています(参考資料9)。安全を期すのであれば、プロセス終了時に最後まで使用するフックハンドラは、「kernel32.dll」が提供するAPIのみで記述したほうが良いでしょう。とはいえ、世の中には有益だがルールを守らないプログラムがたくさん存在しますので、実際のNT Loaderは、とても慎重にDLLの開放順序を決めています(参考資料10)。

次のページ
サンプル3. DirectXランタイムのProcessor Specific Graphics Pipeline(PSGP)を無効化する

修正履歴

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

NyaRuRu(ニャルル)

趣味としてDirectXを嗜んでいたところをMicrosoft MVPとして拾われる。Microsoft MVP for DirectX (Jan 2004-Dec 2006) ここしばらく.NETに浮気中.

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/235 2007/01/15 10:09

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー