Win32デバッガの作り方

戻る

16ビットアプリケーションでは,簡単なデバッグを行うとき,Win16 API OutputDebugString でログを出力させ,Windows3.1 SDK 付属の "DBWin" アプリケーションでログを表示させることができました.
しかし,Win32 では,OutputDebugString が出力するログを表示させる方法は, デバッガ以外にありません.
しかし,デバッガ(Deveroper Studio や Win32 SDK 付属の WinDbg)は, Win32 API OutputDebugString を含むデバッグイベントを受信すると,デバッガに フォーカスが移動することがあるため,ウィンドウのメッセージのトレースを行う ときには大変困ります.そこで,32ビット版 DBWinを作成して,OutputDebugString を表示できるツールを作成して,デバッグログの表示を簡単にする方法について 考えます.

18-1 OutputDebugStringが出力する文字列を取得するには

●32ビットアプリケーションが出力するログを取得するには特別な仕組みが必要
Windows3.1 で Win16 API OutputDebugString でログを出力すると,どのプロセス からでもログを取得することができます.このAPIは,どのプロセスでも参照できる 領域に文字列を設定するため,単一のアドレス空間で動作する16ビット アプリケーションは,設定した文字列の場所を知っていれば取得することが 可能です.
しかし,32ビットアプリケーションは,プロセスはそれぞれ異なるアドレス空間で 動作しています.このため,各プロセスで出力されたログは,別々のアドレス空間に 設定されるため,16ビットアプリケーションのように任意のプロセスからログを 取得することができません.32ビットアプリケーションが出力するログを取得する には,デバッガが,デバッグを行うアプリケーションのアドレス空間にアクセスする ための特別な仕組みが必要になります.
このため,Windows3.1 SDKやWin32 SDK の "DBWin"アプリケーションを起動させても Win32 API OutputDebugString が出力するログを取得することはできません. 現状では,32ビットアプリケーションが出力するログを取得するには, Deveroper Studio や Win32 SDK 付属の WinDbg を利用するしか方法は ありません.

●"Sys.Debugging Log" アプリケーションは"DBWin"と同じもの
Win32 SDK を Windows95 にインストールすると,"Sys.Debugging Log" アプリケーションがタスクバーのグループに登録されます.このアプリケーションは Windows3.1 SDK の「DBWin」と同じものです.
Win32 SDK のデバッグバージョンモジュールをインストールすると,デバッグを行う ために必要な情報を取得することができるようになります.この「DBWin」 アプリケーションは,デバッグバージョンのモジュールをインストールしたシステムの 16ビットモジュールから出力されるログを表示するために利用します.
このため,"Sys.Debugging Log" というタイトルで登録されているのでしょうが, 16ビットアプリケーションが Win16 API OutputDebugStringなどで出力したログに 関しては,Windows3.1 と同様に利用することができます.

図18-1:16ビットアプリケーションでのデバッグ

    +------------+
    |  デバッガ  |
    |            |--+
    |            |  |
    +------------+  |
    |            |  |
    :            :  |
    :            :  |
    |            |  |
    +------------+  |
    | アプリケー |←+
    | ションA    |  |
    +------------+  |
    | アプリケー |←+
    | ションB    |  |
    +------------+  |
    | アプリケー |←+
    | ションC    |
    +------------+
    |            |
    :            :
    :            :
    |            |        同一の空間にすべてのアプリケーションが
    |            |        存在するため,比較的簡単に各プロセスの情報を
    +------------+        得ることができる

図18-2:32ビットアプリケーションでのデバッグ

                       +--------------+
                       |   デバッガ   |
                       |              |
                       +--------------+
                             |
                          アタッチ
                             ↓
+-----------------+ +-----------------+ +-----------------+
|アプリケーションA| |アプリケーションB| |アプリケーションC|
|                 | |                 | |                 |
+-----------------+ +-----------------+ +-----------------+

            各アプリケーションが別々の空間に存在するため
            デバッガはデバッグ対象となるアプリケーションに
            介入してプロセスの情報を取得する必要がある

18-2 デバッガを作成するためのAPI

32ビットアプリケーションが出力したログを取得するには,デバッガと同じ方法で デバッグ情報にアクセスするしかないようです.といっても,デバッガは通常の 32ビットアプリケーションと何ら変わるところはありません.つまり,デバッグを サポートするための API を利用すれば,デバッガでしかアクセスできない情報に 簡単にアクセスできるようになります.

●デバッグAPIとは
32ビットアプリケーションをデバッグすることができるアプリケーションは, システムから送信されるデバッグイベントを受信し,それに応じた処理を行う必要が あります.そして,モジュール(EXEファイル)内に埋め込まれているシンボル情報 を読み出して,ブレークポイントなどの制御に利用します.このため,デバッグ対象と なるアプリケーションのプロセス空間にアクセスする機能を持つ必要があります. これらの機能を実現するために,デバッグAPIを利用します.
デバッグAPIには,デバッガが利用するものと,デバッグ対象となるアプリケーション から実行するものがありますがここでは,基本的なデバッガの行う処理のうち, Win32 API OutputDebugString で出力された文字列を取得するために必要な処理を 中心に説明していきたいと思います.

表18-1:デバッグAPI
VOID OutputDebugString(LPSTR lpszOutputString)
文字列を現在のアプリケーションのデバッガに送る

引数
LPSTR lpszOutputString ... 文字列を指すポインタ

戻り値
なし
BOOL DebugActiveProcess(DWORD dwPid)
アクティブなプロセスにアタッチして,デバッグできるようにする

引数
DWORD dwPid ... 継続するプロセス

戻り値
正常終了 TRUE
異常終了 FALSE
BOOL WaitForDebugEvent(LPDEBUG_EVENT lpde, DWORD dwPid)
デバッグイベントが発生するまで待機する

引数
DWORD dwPid ... 継続するプロセス

戻り値
正常終了 TRUE
異常終了 FALSE
BOOL ContinueDebugEvent(DWORD dwPid, DWORD dwTid, DWORD dwStatus)
デバッグイベントを報告したスレッドをデバッガが継続できるようにする

引数
DWORD dwPid ... 継続するプロセス
DWORD dwTid ... 継続するスレッド
DWORD dwStatus ... 継続状態
 DBG_CONTINUE デバッグを継続
 DBG_EXCEPTION_NOT_HANDLED 例外処理を継続

戻り値
正常終了 TRUE
異常終了 FALSE
BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpvBaseAddr,
 LPVOID lpvBuff, DWORD dwRead, LPDWORD lpdwReaded)
デバッグイベントを報告したスレッドをデバッガが継続できるようにする

引数
HANDLE hProcess ... メモリを読み取るプロセスハンドル
LPCVOID lpvBaseAddr ... 開始するアドレス
LPVOID lpvBuff ... 格納するためのバッファのアドレス
DWORD dwRead ... 読み取るバイト数
LPDWORD lpdwReaded ... 実際に読み取ったバイト数

戻り値
正常終了 TRUE
異常終了 FALSE

その他のAPI
DebugBreak 呼び出し側にブレークポイント例外を発生させる
FatalAppExit アプリケーションを終了させる
FatalExit デバッガに制御を渡す
FlushInstructionCache 命令キャッシュをフラッシュする
GetThreadContext 指定されたスレッドのコンテキストを返す
GetThreadSelectorEntry スレッドのディスクリプタエントリを返しす
SetDebugErrorLevel デバッグ イベントのエラーレベルを設定する
SetThreadContext 指定されたスレッドコンテキストを設定する
WriteProcessMemory 指定されたプロセス内部のメモリに書き込む

●デバッグ対象となるアプリケーション
デバッグ対象となるアプリケーションは,デバッグを行うために追加する処理は 基本的になく,アプリケーションをビルドするときにデバッグオプションを有効に して,シンボル情報をモジュールに埋め込むだけでソースレベルのデバッグが可能に なります.デバッグのために処理を必要とするのは,デバッグを行うために必要な 情報をログとして出力するといったことを行うときくらいでしょう.
デバッグログは,シンボル情報をリンクしなくても出力できるため,比較的気軽に 利用できます.ログの出力処理は,デバッグAPIを利用するよりは,独自にログファイル をオープンして出力したり,WindowsNTでは,ユーザに通知するためのメッセージに 関してはイベントログに出力することもあります.

18-3 デバッガアプリケーション

最近のデバッガは大変高度で,わざわざデバッガをつくるまでもないのですが, デバッグAPIは,他の実行中のプロセスに関する情報を取得するための手段としても 利用できます.しかし,単純に情報を取得だけなら,Windows95のツールヘルプAPI や, WindowsNT のパフォーマンス情報,4.0 からは,イメージヘルプAPIやPSAPI.DLLを 利用することができます.

●デバッグを行うプロセスを起動する
◆CreateProcess
デバッグを行うには,まずデバッグ対象となるアプリケーションを実行する必要が あります.プロセスを実行させるには,Win32 API CreateProcess を利用しますが, 生成フラグにデバッグを行うためのフラグを追加する必要があります.
DEBUG_PROCESS を指定してプロセスを生成すると,呼び出し側プロセスはデバッガ として扱われます.そして,新しいプロセスはデバッグされるプロセス になります.
◆OpenPricess
すでに実行されているプロセスをデバッグするときには,Win32 API OpenProcess を利用して PROCESS_ALL_ACCESS でオープンプロセスハンドルを取得することで CreateProcess でデバッグ用に起動したプロセスと同様にデバッグを行うことが できます.ここで注意しなければならないのは,デスクトップ上で動作している プロセスに対しては,問題なくオープンすることができますが,WindowsNTの サービスプロセスなどのようにシステムが生成したプロセスに対しては,アクセスを 拒否され,OpenProcessは FALSEを返却し,拡張エラーは ERROR_ACCESS_DENIED を 返却します.
(* Windows95では,セキュリティがありませんので,このような処理は必要ない.)

リスト18-1:デバッグを行うプロセスを生成する
CreateProcess(NULL, lpszPathName, NULL, NULL, TRUE,
 DEBUG_PROCESS | CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS,
 NULL, NULL, &si, &pi);

リスト18-2:デバッグを行うプロセスをオープンする
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

●SE_DEBUG_NAME特権を得る
WindowsNT上でシステムが生成したプロセスのデバッグや, Win32 API CreateRemoteThread で他のプロセス空間で任意のスレッドを実行させる 必要のあるアプリケーションはSE_DEBUG_NAME特権をもつ必要があります. この特権は,Administratorグループに属しているユーザやシステムが生成した プロセスでなければ取得することができませんので,Windows NTのサービスプロセスの デバッグを行うデバッガは,Administratorグループのユーザが実行している必要が あります.
一般ユーザが自分のデスクトップから実行するプロセスではこの特権を取得 することはできませんが,サービスプロセスでこれらの処理を行わせるにより, これらの特権処理を実行させることができるようになります. (* 第23章を参照.また,拙著「Win32サブルーチンズ」第5章,第10章を参照)

18-4 デバッガの動作

●デバッグイベントをデバッガに通知するには
システムは,デバッグされるプロセス内で発生するすべてのデバッグイベントを デバッガ(呼び出し側プロセス)に通知します.
Win32 API CreateProcess に,DEBUG_ONLY_THIS_PROCESS をセットしてプロセスを起動 することにより,子プロセスのデバッグイベントを受け取ることができます.
ただし,このフラグをセットして起動されたプロセス(たとえはデバッガでデバッグ 中のプロセス)が,このフラグをセットしてプロセスを生成しても,フラグは無視 されます.

●すでに起動されているプロセスをデバッグするには
プロセスの生成に成功すると,次に Win32 API DebugActiveProcess で, そのプロセスにアタッチします.このAPIに指定したプロセスIDを持つプロセスは, デバッガ自身があたかもDEBUG_ONLY_THIS_PROCESSフラグをセットしてプロセスを 作成したかのようにプロセスへのデバッグアクセスができるようになります.
このAPIを利用すれば,すでに起動されているプロセスに対してもデバッグを行うことが 可能になります.

●デバッグイベント待ちとイベント処理継続
プロセスのアタッチに成功すると,デバッガはWin32 API WaitForDebugEvent を 使用して,デバッグイベントを待ちます.
システムは,プロセス内のすべてのスレッドを中断して,プロセスの現在の状態を 表すデバッガイベントを送ります.このため,マルチスレッドアプリケーションの デバッグを行う場合,普通に実行したときとは動作が異なる可能性があります.
 デバッグイベントは,DebugActiveProcess を呼び出してスレッドに送信されます. デバッグイベントは WaitForDebugEvent で待ち,イベントが発生すると DEBUG_EVENT 構造体に設定して処理を戻します.イベントに応じた処理を実行すると, Win32 API ContinueDebugEvent でイベント処理の継続状態を設定します.

●OutputDebugStringの受信と処理方法
デバッグ処理は,アタッチを行ったスレッドが消滅したり,デバッグ対象となっている プロセスが終了することにより終了します.デバッグの実行中に,アタッチを行った スレッドが消滅すると,デバッグ対象のプロセスも同時に終了してしまいます.

リスト18-3:DEBUG_EVENT構造体

typedef struct _DEBUG_EVENT {
    DWORD  dwDebugEventCode;          /* イベントコード */
    DWORD  dwProcessId;               /* プロセスID */
    DWORD  dwThreadId;                /* スレッドID */
    union{
        EXCEPTION_DEBUG_INFO      Exception;
        CREATE_THREAD_DEBUG_INFO  CreateThread;
        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO    ExitThread;
        EXIT_PROCESS_DEBUG_INFO   ExitProcess;
        LOAD_DLL_DEBUG_INFO       LoadDll;
        UNLOAD_DLL_DEBUG_INFO     UnloadDll;
        OUTPUT_DEBUG_STRING_INFO  DebugString;
        RIP_INFO                  RipInfo;
    } u;
} DEBUG_EVENT;

リスト18-4:デバッグイベント処理

    /* プロセスにアタッチする */
    DebugActiveProcess(dwProcessId);

    while(TRUE){
        /* イベントを待つ */
        if(!WaitForDebugEvent(&debug, INFINITE)){
            return;
        }

        switch(debug.dwDebugEventCode){
            case OUTPUT_DEBUG_STRING_EVENT:   /* デバッグ文字列を受信した */
                break;
            case CREATE_PROCESS_DEBUG_EVENT:  /* プロセスを生成した */
                break;
            case CREATE_THREAD_DEBUG_EVENT:   /* スレッドを生成した */
                break;
            case EXIT_THREAD_DEBUG_EVENT:     /* スレッドが終了した */
                break;
            case LOAD_DLL_DEBUG_EVENT:        /* DLLをロードした */
                break;
            case UNLOAD_DLL_DEBUG_EVENT:      /* DLLをアンロードした */
                break;
            case EXCEPTION_DEBUG_EVENT:       /* 例外が発生した */
                break;
            case RIP_EVENT:                   /* RIPイベント */
                break;

            case EXIT_PROCESS_DEBUG_EVENT:    /* プロセスが終了した */
                return;
            default:
                break;
        }
        /* デバッグを続行する */
        ContinueDebugEvent(dwProcessId, debug.dwThreadId, DBG_CONTINUE);
    }

●OutputDebugString の受信と処理方法
さて,デバッグ対象のアプリケーションが OutputDebugString で出力したログを 受信するには,OUTPUT_DEBUG_STRING_EVENT を処理する必要があります. このイベントを受信すると,OUTPUT_DEBUG_STRING_INFO に情報を設定されます.
この構造体には,デバッグ文字列が格納されている位置やサイズが設定されます.
デバッグ対象のプロセスは,アドレス空間が異なりますので直接アクセスすることが できません.デバッグ文字列も同様に直接アクセスすることはできませんので, Win32 API ReadProcessMemory を利用して,対象となっているプロセスの指定された 領域のスナップショット(写し)を取得します.

リスト18-5:OUTPUT_DEBUG_STRING_INFO構造体

typedef struct _OUTPUT_DEBUG_STRING_INFO {
    LPSTR  lpDebugStringData;      /* デバッグ文字列を指すポインタ */
    WORD   fUnicode;               /* UNICODEであるかを示す */
    WORD   nDebugStringLength;     /* 文字列長 */
} OUTPUT_DEBUG_STRING_INFO;

リスト18-6:デバッグ文字列を取得する

   while(TRUE){
       WaitForDebugEvent(&debug, INFINITE);
       switch(debug.dwDebugEventCode){
           case OUTPUT_DEBUG_STRING_EVENT:
               /* デバッグ文字列を読み出す */
               ReadProcessMemory(hProcess,
                   debug.u.DebugString.lpDebugStringData,
                   szBuff, debug.u.DebugString.nDebugStringLength, &dwRead);
               *(szBuff+dwRead) = '\0';
               break;
           case 
                     :
                     :
                     :
    }

18-5 ロードされたDLLの情報を取得する

●LOAD_DLL_DEBUG_INFO構造体からはDLLのファイル名が取得できない?
DLLがロードされると,そのDLLが持つデバッグ情報へのアクセスを可能とするための 情報を取得することができます.LOAD_DLL_DEBUG_INFO構造体にそれらの情報にアクセス するために必要な情報が設定されます.デバッグ情報は, COFF(Common Object File Format) 形式の情報から派生したものです.
ここで注意しなければならないのは,COFFフォーマットのオブジェクトファイルを 生成するのは Microsoft社のコンパイラのみで,他社製のコンパイラは Intel社の OMF(Object Model Format)を利用しているということです.
このため,複数のコンパイラに対応したデバッガを作成するときには,複数の オブジェクト形式に対応する必要があります.
LOAD_DLL_DEBUG_INFO構造体に設定される情報の中に,DLLのファイル名を取得する ための情報がありますが,少なくとも WindowsNT3.1では正常に取得できないようです. WindowsNT4.0でも試してみましたが,正しく取得できませんでした (方法が間違っていたのかもしれませんが).

●DLLのファイルからDLLのファイル名を取得するには そこで,DLLのファイルからDLLのファイル名が設定される場所を調べて, 取得することにします.しかし,これにはPEフォーマットを持つ実行可能なファイルの フォーマットの構造を知っておく必要があります.
ただし,DLLの情報を取得するための情報が得られるのはロードされるするときで, アンロードされるときには,DLLのベースアドレスのみが渡されます.
このため,アンロード時にもDLLに関する情報を取得したいときには,ロード時に 必要な情報を取得しておく必要があります.
リスト18-7:LOAD_DLL_DEBUG_INFO構造体

typedef struct _LOAD_DLL_DEBUG_INFO {
    HANDLE  hFile;                   /* DLLのファイルハンドル */
    LPVOID  lpBaseOfDll;             /* DLLのベースアドレス */
    DWORD   dwDebugInfoFileOffset;   /* デバッグ情報までのオフセット */
    DWORD   nDebugInfoSize;          /* デバッグ情報のサイズ */
    LPVOID  lpImageName;             /* DLLのファイル名 */
    WORD    fUnicode;                /* DLLのファイル名の文字コードフラグ */
} LOAD_DLL_DEBUG_INFO;

リスト18-7:UNLOAD_DLL_DEBUG_INFO構造体

typedef struct _UNLOAD_DLL_DEBUG_INFO { /* uddi */
    LPVOID lpBaseOfDll;              /* DLLのベースアドレス */
} UNLOAD_DLL_DEBUG_INFO;

18-6 実行可能ファイルのヘッダーフォーマット

●PEフォーマットの実行可能ファイル
PEフォーマット* は,32ビットアプリケーションの実行可能なファイルのヘッダ フォーマットです.MS-DOS アプリケーションは,MZフォーマット,Windows3.1 などで 動作する16 ビットアプリケーションは NE フォーマット,仮想ドライバは LEフォーマット,OS/2 2.0のアプリケーションは LXフォーマットというように, 実行されるシステムによって持っている情報や制御方法が異なることを表わして います.
(* NEフォーマットが持つ情報を見るには,EXEHDR.EXE,PEフォーマットは, DUMPBIN.EXEを利用できます.)

●MZフォーマットとNEフォーマットについて
筆者が学生のころ,NEC製 V25 CPU ベースのマイコンボード上で MS-DOS の EXEファイルを実行させる必要のあるシステムを作成したことがあります.
このとき,EXEファイルのMZフォーマットからリロケーション情報を取り出して, リロケーションを行う必要のあるアドレスをテーブルから読み出して,メモリ中に ロードしたEXEファイルのコード上のアドレスを物理アドレスで上書きして動作させた ことがあります.
16ビットのOS(DOS, Windows)では,1つのアドレス空間に複数のプロセスが実行する ため,EXEファイルがロードされる物理アドレスは,リンクを行うときに特定する ことができません.このため,OSのローダがそのような処理を行う必要 がありました.MS-DOSの構造は単純であるため,この程度のことで実行することが できますが,NEフォーマットではそれ以外にセレクタやセグメントの制御やコード セグメント・データセグメントのロードなどMS-DOSにはない複雑な機能が必要と なります.

●簡単になった32ビットプロセスにおけるローダーの処理
32ビットアプリケーションは,その複雑な構造が一変し,ファイルの ロードはメモリマップドファイルとしてオープンされ,アドレス空間もセグメントが 事実上なくなったためにロードされたモジュールが利用するメモリの管理を行う必要が なくなりました.このため,32ビットプロセスのロータはロードされたDLLの呼び出し アドレスなどの設定を行う必要はありますが,MS-DOSのMZフォーマット程度の簡単な 処理になっています.

●モジュール内の情報取得
デバッガがメモリ中にロードされたモジュール内の情報を取得するには, EXEファイル内に設定された情報を利用してデバッグを行う必要があります.
メモリ中にロードされたモジュールの先頭アドレス(ベースアドレス)から2バイトを 覗くと,スタブルーチンの'MZ'があります.ここからのデータ構造は WINNT.H に 定義してありますので,これを参考にすることができます.

●DLLのファイル名の場所はどこか
 ところで,DLLのファイル名の場所ですが,IMAGE_EXPORT_DIRECTORY に設定 されます.このセクションは,エクスポートされる関数の情報が設定されるので, DLLのファイル名は,次の手順で探し出すことができます.
(1)この場所を知るには,まずPEヘッダの位置を調べる
新しいヘッダの位置は,IMAGE_DOS_HEADER の e_lfanew メンバに設定されます.
(2)RVA(Relative Virtual Address)を取得する
メモリ中に配置されたイメージの情報にアクセスするにはこのアドレスとモジュール がロードされたアドレス(ベースアドレス)を足すことで得られます.
DLLのファイル名は,IMAGE_EXPORT_DIRECTORYにあり,この情報を指すRVAは, IMAPGE_OPTIONAL_HEADER の中にある IMAGE_DATA_DIRECTORY の最初に設定され, WINNT.H に
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
と定義されています.これにより,IMAGE_DATA_DIRECTORYの配列の最初にあることが 分かります.この構造体の ImageBase メンバがIMAGE_EXPORT_DIRECTORYの RVAと なります.
(3)DLLのベースアドレス + IMAGE_EXPORT_DIRECTORY構造体のNameメンバのオフセット からメモリにロードされているDLLのファイル名のRVAを,取得する
(4)DLLのベースアドレス + DLLのファイル名のRVA が指す領域から,ファイル名を 取り出す

図18-3:PEフォーマットを持つ実行可能ファイルの構造
+------------------------------+
|'MZ'                          |
|      IMAGE_DOS_HEADER        |
|                              |
+------------------------------+
|'PE\0\0'                      |
|      IMAGE_NT_HEADERS        |
|                              |
|+----------------------------+|
||     IMAGE_FILE_HEADER      ||
|+----------------------------+|
||                            ||
||    IMAGE_OPTIONAL_HEADER   ||
||                            ||
|+----------------------------+|
||  IMAGE_DATA_DIRECTORY[0]   ||
|+----------------------------+|
||  IMAGE_DATA_DIRECTORY[1]   ||
|+----------------------------+|
|:             :        [n]   :|
|+----------------------------+|
+------------------------------+
| IMAGE_SECTION_HEADER (.text) |
+------------------------------+
| IMAGE_SECTION_HEADER (.data) |
+------------------------------+
:              :               :
:              :               :
+------------------------------+
|           .text              |
|                              |
+------------------------------+
|           .data              |
|                              |
+------------------------------+
:              :               :
:              :               :
+------------------------------+
|         COFF行番号           |
+------------------------------+
|         COFFシンボル         |
+------------------------------+
|                              |
|         デバッグ情報         |
|                              |
+------------------------------+

18-7 ウィンドウハンドルからプロセスIDを取得するには

デバッガがすでに実行しているプロセスにアタッチする場合,必ず必要なのは アタッチするプロセスのプロセスIDですが,アプリケーションから実行中のプロセスの プロセスIDを取得するのは意外と面倒な処理になります.
(* 拙著「Win32サブルーチンズ2」第2章を参照.)
そこで,もう少し簡単にプロセスIDを取得するために, Win32 API GetWindowThreadProcessId を利用します.このAPIを利用すると, ウィンドウを持つアプリケーションについてはプロセスIDを取得することができます. 現在存在するウィンドウは,Win32 API EnumWindows で簡単に列挙することができます ので,取得したウィンドウを選択できるようにすれば,アタッチを行うウィンドウを 選択することができるようになります.

18-8 OutputDebugStringへ出力を行う printf関数

Win32 API OutputDebugString は指定された文字列を出力しますが,実際にデバッグ で利用するときには,エラーコードや戻り値などの数値を出力することが多いのでは ないでしょうか?
そこで,OutputDebugString への出力に書式を付ける関数を作成します. ビルドするモジュールがデバッグビルドのときだけ出力を行いたいときには, たとえば DEBUG マクロが定義されているときだけ定義されるようにすると,便利に なるのではないかと思います.デバッグビルドでないモジュールでログの出力を行う ことも当然可能で,モジュールがデバッガにアタッチされていなげれば,出力された ログをすべて無視されます.

リスト18-9:Win32 API OutputDebugString を printf 化する

#include <windows.h>
#include <stdio.h>

    void cdecl DbgPrintf(LPSTR, ...);


void DbgPrintf(LPSTR lpszFmt, ...)
{
    LPSTR    *lppParam;
    char    szBuff[256];

    lppParam = ((LPSTR*)&lpszFmt) + 1;
    vsprintf(szBuff, lpszFmt, (LPSTR)lppParam);
    strcat(szBuff, "\n");
    OutputDebugString(szBuff);
}

18-9 プログラムについて

●DBWinもどき32ビット版
Windows3.1 SDK の DBWin のような動作をする,ログ表示ツールです. 実は,WindowsNT3.5のアプリケーションを開発しているころから,いつか Microsoftが Win32 SDK に DBWin の32ビット版を付けてくれるのを待ったいましたが,どうもその 気配はないようなので,DBWinもどきの32ビット版を作成することにしました.
しかし,本家の "DBWin" ほど高機能ではなく,単純にデバッグログを採取するだけで, 他に機能はありません.また,ログの表示はエディットコントロールを利用して いますので,バッファサイズが32Kバイトに制限されます.
このサイズでは簡単にオーバーフローしてしまいますので,オーバフローしそうに なると,ログの上半分を削除してログの採取を続けるようにしてあります.
16ビットでは,DBWinを起動するだけですべての16ビットアプリケーションのログを 取得することができますが,32ビットプロセスでは,デバッグ対象のアプリケーション にアタッチする必要があります.同時に複数のアプリケーションをアタッチすることも できますが,アタッチしないアプリケーションの情報は取得できません.
本サンプルアプリケーションでは,アタッチメニューから「ウィンドウを選択する」 または「PIDを指定する」でアタッチするプロセスを選択することで,すでに実行 されているアプリケーションに対してアタッチすることができます.
このアプリケーションが有効なのは,ウィンドウのメッセージをトレースするとき や,開発環境をインストールされていないマシンでログを採取するときくらいでしょう が,最近のデバッガは高機能ではありますがそれなりに巨大化していますので,起動に 時間がかかるなど使い勝手がよくないこともあります.しかも,デバッグやテストで 動作を確認する場合,複雑な機能が必要となることはあまりなく,簡単なログ出力機能 でも十分に活用することができます.

●ソースファイル
DBWIN32.EXE
 DBWIN32.C (DBWin32本体)
 WINPID.C (ウィンドウ選択ダイアログボックス)
 EDITPID.C (PID入力ダイアログボックス)
 DBWIN32.H (リソースファイル)
 DBWIN32.RC (リソースファイル)
 DBWIN32.ICO (アイコンファイル)
 MAKEFILE (メイクファイル)
TEST.EXE
 TEST.C (ログを出力するアプリケーション)
 TEST.H (ヘッダファイル)
 TEST.RC (リソースファイル)
 MAKEFILE (メイクファイル)