SHOEISHA iD

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

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

特集記事

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

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


  • X ポスト
  • このエントリーをはてなブックマークに追加

サンプル1. アプリケーション起動時にRetail/Debug Runtimeを選択する

 DirectXにはエンドユーザ向けのRetail Runtimeの他に、開発者向けのDebug Runtimeも提供されています。通常のDirectX再配布ファイルではDebug Runtimeはインストールされません。Debug Runtimeの動作には次のような違いがあります。

  • 実行時により入念なパラメータ検証が行われるようになる。
  • 開発に役立つさまざまな情報をOutputDebugString APIでデバッガに出力してくれる。

 開発者向けのDirectXランタイムをインストールすると、コントロールパネルにDirectXというアプレットが追加され、ランタイムの設定を行えるようになります。

図6 DirectXのプロパティ
図6 DirectXのプロパティ
図7 DirectXのプロパティ 「Direct3D」タブ
図7 DirectXのプロパティ 「Direct3D」タブ

 残念ながら、この設定アプレットと同等の設定を行う専用APIは用意されていません。この章では、デバッグランタイムとリテールランタイムのどちらを使用するか、プログラムから制御する方法について検討を行います。

 RegMonFileMonでモニタリングしてみると、設定情報は主にレジストリと「%windir%\win.ini」に格納されていることが分かります。図8は、RegMonによってモニタリングしている様子を示しています。

図8 RegMonによるモニタリング
図8 RegMonによるモニタリング

 表2は、Direct3Dタブで設定できるパラメータの設定場所を示しています。

表2 表示項目とデータ格納場所の対応
表示項目データ格納場所
Debug/Retail D3D RuntimeHKLM\Software\Microsoft\Direct3D\LoadDebugRuntime
Enable Shader DebuggingHKLM\Software\Microsoft\Direct3D\EnableDebugging
Maximum ValidationHKLM\Software\Microsoft\Direct3D\FullDebug
Break on Memory LeaksHKLM\Software\Microsoft\Direct3D\BreakOnMemLeak
Break on D3D ErrorHKLM\Software\Microsoft\Direct3D\BreakOnDPF
Enable Multi-mon DebuggingHKLM\Software\Microsoft\Direct3D\EnableMultimonDebugging
Allow Hardware AccelerationHKLM\SOFTWARE\Microsoft\Direct3D\Drivers\SoftwareOnly
Enumerate Ramp RasterizerHKLM\SOFTWARE\Microsoft\Direct3D\Drivers\EnumRamp
Enumerate Reference RasterizerHKLM\SOFTWARE\Microsoft\Direct3D\Drivers\EnumReference
Debug Output Levelwin.ini [Direct3D] debug=x (x = 0 - 4)
図9 設定が書き込まれているレジストリ位置
図9 設定が書き込まれているレジストリ位置

 設定の格納場所が分かったことで、アプリケーション起動直前に「HKLM\Software\Microsoft\Direct3D\LoadDebugRuntime」 を変更することにより、使用するランタイムを変更できるようになりました。しかし、レジストリの書き換えには権限が必要なことと、書き換えによってシステム全体に影響が及ぶことを考慮し、今回はよく知られたAPIフック (参考資料5)の手法を用いることにします。DirectXが実際にレジストリにアクセスする前にAPI呼出しを横取りすることで、レジストリに設定する必要を無くしてしまおうというわけです。

 WindowsでのAPIフックにはいくつか方法が知られていますが(参考資料56)、今回は自プロセスのアドレス空間にあるインポートアドレステーブル(IAT)を書き換える方法を採用しています。この方法であれば影響範囲は自分自身のプロセスに限定されます。

 実際にフックを行うHook関数は「apihook.h」と「apihook.cpp」で実装されています。この関数は、x64アプリケーション内でも正しく動くように作ってあります。

Hook関数のプロトタイプ宣言
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関数で置き換えたい場合、次のように使用します。

Hook関数の使い方
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のさまざまな引数のパターンと、RegGetValueRegEnumValueなどのAPI、そして各APIのUnicodeバージョンも考慮する必要があります。

 またこのフックハンドラは、hKeyレジストリキーハンドルを確認しないという問題があります。そのため、このハンドラはレジストリキーの場所に関係なく、「LoadDebugRuntime」という値に対するアクセス全てを横取りしてしまいます。hKeyがどのレジストリキーを指しているか取得するAPIがあればよいのですが、探した限りでは見つけることができませんでした。考えられる対策は、レジストリキーを取得できるAPI全てをフックして有効なハンドルを追跡することです。

 デバッグランタイム以外の多くの設定についても、レジストリAPIのフックで対応できますが、唯一"Debug Output Level"の設定については、「win.ini」ファイルへのアクセスを横取りする必要があります。

コラム4 マルチスレッドCRT
 フックハンドラは、複数のスレッドから同時に呼び出されるかもしれません。必要に応じて排他制御の導入や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版)
 「Callback.dll」には、以下のような関数DoWorkが実装されています。
「Callback.dll」の実装
CALLBACK_API HANDLE DoWork(WorkProc workProc)
{
    return (HANDLE) _beginthreadex(
        NULL, 0, workProc, NULL, 0, NULL );
}
 exeファイルは、次のように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;
}
 このことから、別のスレッドで実行されるかもしれないコールバックハンドラを作るときの指針として次のことが言えます。すなわち、DLLとしてハンドラを作成するか、exeファイルにハンドラを書くならCRTと動的リンクすべきだということです。もちろん、ハンドラの作成にCRTを使わないということであればこの限りではありません。

 デバイスの作成が終わったら、フックを元に戻しておきます。これはオリジナルの関数ポインタを書き戻すだけです。これで「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;
}
コラム5 フックの解放
 以下のコードは、「test.dll」が既に読み込まれていればGetProcAddress APIでエクスポート関数を取得し、これを呼び出すというものです。しかし、GetProcAddress APIを使用してもOSはDLLの参照カウントを増やしませんから、MyFuncを実行中に別のスレッドが「test.dll」をアンロードすることは原理的に可能です。これを回避するためには、呼び出し元でLoadLibrary APIを使用して「test.dll」の参照カウントを増やしておく必要があります。
DLLのエクスポート関数呼び出し(危険バージョン)
HMODULE module = GetModuleHandleA( "test.dll" );
PROC proc = (module != NULL ?
    (PROC)GetProcAddress( module, "MyFunc" ) : NULL);
if( proc != NULL )
{
    proc();
}
 IATを書き換えるフックの解放についても同じことが言えます。フックを解放しようとしたとき、いくつかのスレッドでは、まだフックハンドラを実行中かもしれません。あるいはフックハンドラのアドレスがCPUレジスタに読み込まれ、今まさにジャンプしようとしているところという可能性もあります。
 常駐アプリケーションなどによって他人のフックコードが紛れ込んでくることもあります。IATの書き換えがほぼ同時に行われた場合、IATのページ属性の変更に競合が発生するかもしれません。
 無事にIATの書き換えが行われたとしても、書き換える順序は依然として問題になりがちです。実際に呼び出されるのは後から書き込んだ方のハンドラです。我々が先にフックを仕掛けていたとすれば、後から来てフック行おうとするルーチンにとって、我々のフックハンドラは「オリジナルの関数アドレス」となります。このことが正確なアンフックを難しくします。例えば以下の順序でフック解除を行うと、最終的にはAさんのフックハンドラが残ってしまいます。
  1. Aさんがフックを作成
  2. Bさんがフックを作成
  3. Aさんがフックを解除
  4. Bさんがフックを解除
 これはSetWindowLongAPIを利用したウィンドウのサブクラス化についても同じことが言えます。信頼性の高いフック機構を設計するために、SetWindowsHookExAPIやCallNextHookEx APIなどの実装と、一度比較してみるとよいでしょう。これらのAPIではフックの寿命はハンドルによって管理され、フックの挿入と削除は必ず関数を介して行います。

次のページ
サンプル2. DirectXランタイムが出力するデバッグメッセージのログを作成する

修正履歴

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

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

NyaRuRu(ニャルル)

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

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

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

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/235 2007/01/15 10:09

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング