2009年06月24日

Windowsプログラムの異常終了をトラップするコード

はてなブックマークに登録

■ はじめに

先日、社内でこういう話題がありました。
「Windows 上のプログラム A からソースコードのないプログラム B を起動する必要があるんだけど、プログラム B はときどき異常終了しちゃったりする。内輪用だから落ちること自体は目をつぶるとして、プログラム B が異常終了した場合にはプログラム A 側でそれを上手にハンドルしたい。良い方法はないものか」

この話に興味を感じ、異常終了を起こす短いプログラムを作ってデバッガでトレースしながらヒントを探している内にふと思いました。


このプログラムを裸で実行すると、プログラム内で処理されない例外は図のような形でシステムによって処理されます。しかし、デバッガ上でデバッギ(デバッグ対象)として実行している場合はデバッガが例外の発生を検知しそれをユーザに伝えます。つまり、デバッガの制御下にあります。

ということは「デバッガとして動作するコード」を用意しプログラム B をデバッギとして扱えば、そこで発生した未処理の例外を捕捉することができるかもしれません。 ちょっと面白そうなので試しにそういうプログラムを書いてみることにしました。

■ どういうものを作る?

作りたいプログラムのイメージはこういうものです。
  1. 指定されたプログラムを子プロセスとして実行する
  2. 子プロセス内で未処理の例外が発生したら所定の終了コードを返す
  3. 子プロセスが正常に終了した場合にはその終了コードを返す
つまり一種のラッパプログラムですね。

■ どうやって作る?

デバッガを使ったことのないプログラマは少ないと思いますが、デバッガを作ったことのあるプログラマもまたあまり多くはないでしょう。筆者にもその経験はなく、なんだか「特別な世界」というイメージがあります。もっとも今回の目標はプログラムをデバッグするためのデバッガを作ることではなくあくまでもデバッガの機能の一部を利用することですからあまり気にせず実現方法を調べることにします。

上に書いた通りこのプログラムは子プロセスを起動するものです。 何はともあれ CreateProcess API の説明をあらためて読んでみます。
MSDN ライブラリ - CreateProcess 関数
6 番目のパラメータの dwCreationFlags の説明に次の記述があります。
DEBUG_PROCESS
このフラグを指定すると、呼び出し側プロセスをデバッガ、 新しいプロセスをデバッグ対象として扱います。システムは、デバッグ対象の プロセス内で発生するすべてのデバッグイベントを呼び出し側スレッドへ通知します。
これこれこれ!という感じで、「デバッグイベント」というそそられるキーワードも出てきました。 ここにはこれ以上の説明は見あたりません。「DEBUG_PROCESS」で調べてみます。

MSDN ライブラリ - Process Creation Flags (Windows)
DEBUG_PROCESS 0x00000001
The calling thread starts and debugs the new process and all child processes created by the new process. It can receive all related debug events using the WaitForDebugEvent function.
デバッグイベントというものは WaitForDebugEvent 関数で受け取ることができるということですね。

MSDN ライブラリ - WaitForDebugEvent 関数
デバッグ中のプロセスでデバッグイベントが発生するのを待機します。

BOOL WaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,
  DWORD dwMilliseconds
);

パラメータ
lpDebugEvent
待機するデバッグイベントの情報が入った 構造体へのポインタを指定します。
dwMilliseconds
デバッグイベントを待機する時間をミリ秒単位で指定します。
DEBUG_EVENT 構造体を調べてみます。

MSDN ライブラリ - DEBUG_EVENT Structure (Windows)
typedef struct _DEBUG_EVENT {
  DWORD dwDebugEventCode;
  DWORD dwProcessId;
  DWORD dwThreadId;
  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, *LPDEBUG_EVENT;


dwDebugEventCode
The code that identifies the type of debugging event. This member can be one of the following values.

  • CREATE_PROCESS_DEBUG_EVENT
  • CREATE_THREAD_DEBUG_EVENT
  • EXCEPTION_DEBUG_EVENT
    Reports an exception debugging event. The value of u.Exception specifies an EXCEPTION_DEBUG_INFO structure.
  • EXIT_PROCESS_DEBUG_EVENT
  • EXIT_THREAD_DEBUG_EVENT
  • LOAD_DLL_DEBUG_EVENT
  • OUTPUT_DEBUG_STRING_EVENT
  • RIP_EVENT
  • UNLOAD_DLL_DEBUG_EVENT

Remarks
If the WaitForDebugEvent function succeeds, it fills in the members of a DEBUG_EVENT structure.

ポイントは、WaitForDebugEvent 関数が成功すると引数で渡された DEBUG_EVENT 構造体の各メンバがセットされること、例外が発生した場合は dwDebugEventCode に EXCEPTION_DEBUG_EVENT が セットされること、EXCEPTION_DEBUG_EVENT の時は u.Exception メンバの EXCEPTION_DEBUG_INFO 構造体を参照せよ、ということのようです。

これで大体の仕組みがわかりました。あとは不明点を調べながらコードを書くことにします。

■ 試作したコード (trapfault.c)

荒削りながらざっくりコードを書いてみました。
trapfault.exe はこういうものです。
引数 1:子プロセスで未処理の例外が発生した際に呼び元へ返す終了コード
引数 2:子プロセスとして起動する実行ファイル名 # フルネームで指定
引数 3〜:子プロセスの起動時に渡すパラメータ # 省略可

終了コード:子プロセスが正常終了すればその終了コードを返し
異常終了すれば第一引数で指定されたコードを返す
#include <windows.h>
#include <stdio.h>

DWORD doit(int ac, char *av[])
{
  BOOL bSts;
  STARTUPINFO si;
  PROCESS_INFORMATION pi;
  DWORD dwExitCode;
  DEBUG_EVENT debug;
  DWORD dwContinueStatus;
  DWORD dwDebugEventCode, dwDebugEventCodePrev = 0;
  DWORD dwExceptionCode, dwExceptionCodePrev = 0;
  DWORD dwExceptionFlags, dwExceptionFlagsPrev = 0;
  ULONG_PTR dwExceptionInformation0, dwExceptionInformation0Prev = 0;
  ULONG_PTR dwExceptionInformation1, dwExceptionInformation1Prev = 0;
  char *pszExe = av[2];
  char szParam[1024] = "\0";

  if (ac > 3) { // 子プロセスへ渡す引数
    int i;
    for (i = 3; i < ac; i++) {
      strcat(szParam, " ");
      strcat(szParam, av[i]);
    }
  }
  memset(&pi, 0, sizeof(pi));  
  memset(&si, 0, sizeof(si));  
  si.cb = sizeof(si);

  // 指定されたプログラムのプロセスを起動
  bSts = CreateProcess(pszExe, szParam, NULL, NULL, FALSE,
      DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS |
      CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS,
      NULL, NULL, &si, &pi);

  if (!bSts) {
    // CreateProcess 失敗
  }

  // デバッグイベントループ
  while (WaitForDebugEvent(&debug, INFINITE)) {
    dwDebugEventCode = debug.dwDebugEventCode;
    dwContinueStatus = DBG_CONTINUE;

    // 例外発生
    if (dwDebugEventCode == EXCEPTION_DEBUG_EVENT) {
      dwExceptionCode =
        debug.u.Exception.ExceptionRecord.ExceptionCode;
      dwExceptionFlags =
        debug.u.Exception.ExceptionRecord.ExceptionFlags;
      dwExceptionInformation0 =
        debug.u.Exception.ExceptionRecord.ExceptionInformation[0];
      dwExceptionInformation1 =
        debug.u.Exception.ExceptionRecord.ExceptionInformation[1];

      // シングルステップ例外, ブレークポイント例外は無視
      if (dwExceptionCode != EXCEPTION_SINGLE_STEP &&
          dwExceptionCode != EXCEPTION_BREAKPOINT) {

        // 直前と同一の例外なら脱出
        if (dwDebugEventCodePrev == EXCEPTION_DEBUG_EVENT &&
          dwExceptionCodePrev == dwExceptionCode &&
          dwExceptionFlagsPrev == dwExceptionFlags &&
          dwExceptionInformation0Prev == dwExceptionInformation0 &&
          dwExceptionInformation1Prev == dwExceptionInformation1) {
          dwExitCode = (DWORD)atol(av[1]);
          break;
        }
        dwDebugEventCodePrev = EXCEPTION_DEBUG_EVENT;
        dwExceptionCodePrev = dwExceptionCode;
        dwExceptionFlagsPrev = dwExceptionFlags;
        dwExceptionInformation0Prev = dwExceptionInformation0;
        dwExceptionInformation1Prev = dwExceptionInformation1;
        // プログラム側の例外処理にまかせてみる
        dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
      }
    }
    // プロセス終了
    else if (dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
      if (debug.dwProcessId == pi.dwProcessId) {
        // デバッギの終了コードをセット
        GetExitCodeProcess(pi.hProcess, &dwExitCode);
        break;
      }
    }
    // イベントループ継続
    if (!ContinueDebugEvent(debug.dwProcessId,
                debug.dwThreadId, dwContinueStatus)) {
      break;
    }
  }
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);

  return dwExitCode;
}

int WINAPI WinMain(HINSTANCE p_hInstance, HINSTANCE p_hPreInst,
                   LPSTR p_pchCmdLine, int p_iCmdShow )
{
  char buf[16];
  int ret;
  if (__argc < 3 || GetFileAttributes(__argv[2]) == 0xFFFFFFFF) {
    MessageBox(NULL,
      "usage: trapfault <exitcode> <debugee> [<debugee parameter>]...",
      __argv[0], MB_OK);
    return 0;
  }
  ret = (int)doit(__argc, __argv);

  //sprintf(buf, "ret=%d\n", ret);
  //MessageBox(NULL, buf, __argv[0], MB_OK);

  return ret;
}
デバッガとして実装しなければ難しいことをひとまずこの程度の短いコードで実現できました。 今回は単純なものでしたが、この方法はいろいろ応用できそうです。

■ ダウンロード

上記のプログラムの実行形式をここからダウンロードできます(フリーソフト)。詳細はアーカイブに含まれる README を参照して下さい。

・「trapfaultMbox 999 test1.exe」の実行結果

・「trapfaultMbox 999 test1.exe abc」の実行結果

■ 参考書籍

APIで学ぶWindows徹底理解
 日経BP社刊 安室 浩和 著
Windowsプログラマのためのデバッグテクニック徹底解説
 日経BPソフトプレス刊 John Robbins 著 豊田 孝 訳
(tanabe)
klab_gijutsu2 at 10:59│Comments(2)TrackBack(0)win 

トラックバックURL

この記事へのコメント

1. Posted by レンタルサーバー比較管理人のタカ   2009年11月11日 12:45
どうも、始めまして、
タカです。
記事読ませて頂きました。

ソースコードのないプログラムを起動さえることが可能なんですか。。
凄い。。

デバッガ等を作れるのは、
プログラマーでは少ないんですか。
難易度高めなんでしょうか?
2. Posted by tanabe   2009年11月11日 16:27
タカさん、コメントをありがとうございます。もしこの記事の内容に関心をお持ちなら参考書籍を一読されることをおすすめします。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント