エラー処理・デバッグ [Errors]


1. ◆ エラーを発生させるケース

エラー発生機能は、続きの処理が実行できないときに使うものなので、 不正な値をとったときのすべてのケースでエラー発生機能を使うこと (例外を発生させること)がベストであるとは限りません。

表示しようとしているデータが不正な値になっているときは、 エラーを発生するのではなく、エラーであることを表示します。 エラー表示したら続いて次のデータを表示することができます。


2. ◆ エラー発生機能

エラーは、ASSERT マクロや error マクロによって発生します。
エラーを発生させた場合、デフォルトではプログラムを強制終了します。 ただし、Errors モジュールがプログラムを強制終了する前に 実行するエラーハンドラを登録すれば、 プログラム終了時の動作をプログラミングすることもできます。

マクロ名内容
ASSERTデバッグ版のみのチェック(省略形)
ASSERT2_0デバッグ版のみのチェック(完全形)
errorエラー発生(省略形)
error2_0エラー発生(完全形)
error_clearエラークリア

2-1-1. マルチタスク環境での動作について

エラーメッセージの記憶領域がタスクごとに用意されているなど、 本機能はマルチタスク環境でも正しく動作します。 ただし、マルチタスク環境では、あるタスクにエラーが発生しても、 システムの一部はまだ動いているので、エラーハンドラの処理内容に 注意が必要です。


3. ◆ エラーメッセージの確認方法

エラーメッセージを確認するには、次の方法があります。

プログラムの開発工数の削減のため、エラーメッセージが ソースファイル名と行番号のみになることがよくあります (error 関数または ASSERT を使用)。 この場合、ソースファイルを参照してヒントを得てください。 ただし、よく発生するエラーに関しては、詳細なエラーメッセージを 表示するようにしていますので、要望があれば開発者に相談してください (error2_0 マクロや ASSERT2_0 等を使用)。

エラーが発生した位置を確認するには、いくつかの方法があります。

3-1. コンソール・アプリケーションの出力を確認するには

Visual C++ 開発環境でコンソール(コマンドライン)アプリケーションを デバッグ実行してエラーが発生したとき、 エラーメッセージを表示してすぐに コマンドプロンプト・ウィンドウが消えてしまいます。

ウィンドウが消えないようにしてエラーメッセージを確認するには、 errors.c のプログラム終了ポイント([EXIT_POINT] で検索) にブレークポイントを張ってください。


4. ◆ エラーハンドラ

エラーハンドラは、エラー発生機能を使用してエラーが発生したときに、 呼び出される関数です。
エラーハンドラ(関数)はアプリケーションプログラマが作成し、 main 関数のはじめの方(エラーが発生する前)で 登録する関数を呼び出すようにします(Errors_setErrorHandler マクロ)。
デバッグするときは、エラーハンドラにブレークポイントを貼っておくと良いでしょう。

4-1-1. エラーハンドラのサンプル

int  err( int id, int code, char* msg )
{
  #ifndef  NDEBUG
    if ( code == Errors_ASSERT )
      Errors_break( 0xEEEE );
  #endif

  if ( code != Errors_ASSERT && code != Except3_Err_OutOfTry )
    c_throw( Errors_Msg_getGlobl() );

  #ifdef  NDEBUG
    /* 下の2行のうちどちらか */
    Errors_printf_release( "%s", msg );
    Errors_MsgPool_print( Errors_MsgPool_getGlobl() );
  #endif

  return  ERRORS_EXIT;
}

4-1-2. エラーハンドラの登録

エラーハンドラを登録するには、Errors_setErrorHandler マクロを使用します。 メイン関数の最初で実行してください。 登録できる関数は1つだけなので、2回以上 Errors_setErrorHandler マクロを 呼び出すときは、注意してください。また、マルチタスク環境でも 登録できる関数は1つだけなので、どのタスクにエラーが発生しても 同じエラーハンドラが呼び出されるので注意してください。 複数のエラーハンドラを登録して呼び出したいときは、 その機能を持った関数をエラーハンドラ内で呼び出してください。

4-1-3. エラーハンドラのインターフェイス

エラーハンドラは、次のような形式になっています。
int  err( int id, int code, char* msg );

エラーコードとエラーメッセージには、エラー発生機能で指定された ものが入っています。ですので、エラーの内容を知る場合、 それぞれのモジュールのエラーコードを確認するか、 エラーメッセージを表示させてください。 ただし、エラーメッセージは容量を食うので、Errors モジュールの 設定によっては内容が入っていないこともあります。
引数 id は、エラーが起きるたびに +1(初期値は1)する値が 代入されています。これにより、デバッグ時に無視したいエラーと 無視したくないエラーを迅速に判断できるようになります。 また、他人が発生させたエラーと同じエラーであることを 確認する手段の1つにもなります。
返り値は、次の定数を指定します。

ERRORS_EXIT を指定した場合、Errors_errPrintf によるエラーメッセージの 出力と、後始末チェックを行います。

4-1-4. エラー処理の内容

エラーハンドラで処理する内容は、次のようなものが考えられます。

エラーの内容を通知するには、Errors_errPrintf を使用することが できます。独自の通知方法を使ったり、デバッガのウォッチ機能を 使っても構いません。


5. ◆ エラーからの復旧について

エラー発生機能を使用してエラーが発生したら、基本的にプログラムを 終了する(exit)かリセットするしかありません。
しかし、ソフトウェア例外処理(Except3)を使用することで、 エラーから復旧することもできます。 詳細は、例外処理のドキュメント中の「例外処理の実装手順と記述例」の項を 参照してください。

Except3 などを使用して例外を発生させた場合、 エラーハンドラからリターン(return 文を使用) しなくても構いません。 try ブロックの外へ例外メッセージが投げられたときは、 エラーコードが Except3_Err_OutOfTry で 再びエラーハンドラが呼び出されます。 そのときは、返り値を ERRORS_EXIT にするなどして プログラムを強制終了するようにしてください。


6. ◆ 推奨するデバッグ手順

デバッグは次のようにすると、素早く行うことができます。

  1. エラーが発生する入力条件やモジュール構成を簡単にする。
  2. WD,WS,WP,WF マクロや Errors_printf などを使って変数値を確認し、 異常な値を確認したら、それが ASSERT されるかエラーになるようにする。
  3. COUNT マクロを使って、エラーが発生するタイミングを特定する。
  4. 関数コールスタックを見たいときは、MARK マクロを使って エラーが発生するソースの位置をトップダウンに特定していく。 または、ERRORS_USE_FUNC_LOG マクロを使用して関数のコール履歴を参照する(対応予定)。
  5. 異常な値を格納するソースの位置を、WATCH マクロを使ってトップダウンに特定していく。

7. ◆ デバッグ用表示について

ただ単に、データを表示するだけなら WD 等のマクロを使用すれば済みますが、 Errors_printf を用いて、 データの意味を含めて整形された表示を行うようにすれば、 データの意味を考えることが少なくなり、 デバッグを速やかに行うことができるようになります。

整形された表示を行うには、整形して表示するための工数がかかりますが、 オブジェクト(構造体)ごとにそのデータを表示するような関数を 作成すれば、流用が効くようになります。
たとえば、Staff 構造体があれば、Staff_print という関数を作成します。

struct Staff {
  char  name[80];
  int   age;
  Staff*  partner;
};

void  Staff_print( Staff* this )
{
  Errors_printf( "Staff(%p)[%s]: age=%d, partner=(%p)%d",
    this, this->name, this->age, this->partner, this->partner->name );
}

表示する内容に、構造体名と構造体のアドレスを含むようにすると デバッグ時に便利です。
上のような表示関数は、デバッグ用なので ERRORS_CUT_DEBUG_TOOL で リリース時にカットできるようにしておきます。

ある特定の関数のデバッグの頻度が大きい場合は、 その関数の内部にデバッグ表示関数を埋めこみ、 #if ERRORS_DEBUG_FALSE または #if ERRORS_DEBUG_TRUE で囲むようにすると、 表示非表示を簡単に切替えることができるようになります。


8. ◆ 関数コール履歴について

関数コール履歴は、スタックに積まれた関数コールの様子を記録しています。 コールスタックを備えていないデバッガで、コールスタックと同じことを 行います。

既に関数コール履歴に対応しているモジュールを使用しているときに、 コールスタックを見たいときは、次のように記述します。

  1. Errors モジュールのモジュール設定の ERRORS_USE_FUNCLOG を #define してすべて再コンパイル
  2. main 関数で ERRORS_FUNCLOG_INIT を記述
  3. 履歴を参照したいところで Errors_FuncLog_print を記述 (エラーで止まるときは、デフォルト・エラーメッセージと一緒に表示します)

関数コール履歴に対応した関数にするには、以下の記述が必要になります。
int  func()
{
  int  local;

  ERRORS_FUNC_START( func );   /* 関数の始まり */

  local = sub();
  local *= 2;

  ERRORS_FUNC_END( func );   /* 関数の終わり */

  return  local;
}
ERRORS_FUNC_END は、関数の途中でリターンするところにも 記述する必要があります。

ERRORS_FUNCLOG_PRINT_EVERY マクロを定義すると、 関数に出入りするたびに Errors_printf による処理中の関数の表示を行います。
ただ、そのまま表示しようとすると、デバッグ表示がとても多くなるので、 関数コール履歴表示の洗練を行います。 ただし、関数に入ったかどうかをトレースできなくなるので、 注目している関数がトレースされるか注意してください。
ERRORS_FUNCLOG_SPECIFIC マクロを定義すると、 特定のソースファイルだけ関数コール履歴を有効にします。 関数コール履歴を有効にするソースファイルの先頭に、 以下のような #include 文を記述します。 (errors_fs.h は、errors.h のあるフォルダと同じところにあります)

#if  ERRORS_DEBUG_TRUE
#include "C:\mo\02\COMPONE\TODA\src\errors_fs.h"
#endif

関数コール履歴表示の洗練を行う場合、 ERRORS_FUNC_START2 ERRORS_FUNC_END2 を用いて、 レベル指定を行います。 以下のキーワードを ERRORS_FUNC_START2 ERRORS_FUNC_END2 の第1引数に指定します。 上にあるものほど制限がきついときでも表示されます。
#define  ERRORS_FUNC_MAIN_INF   2       /* 特に表示するインターフェイス */
#define  ERRORS_FUNC_MAIN_IMP   3       /* 特に表示するインプリメント */
#define  ERRORS_FUNC_INF   4            /* レベル指定を行わなかったときのデフォルト */
#define  ERRORS_FUNC_IMP   5
#define  ERRORS_FUNC_CRITICAL_INF   6
#define  ERRORS_FUNC_CRITICAL_IMP   7   /* 表示を制限するインプリメント */

  ERRORS_FUNC_START2( ERRORS_FUNC_INF, func );   /* 関数の始まり */
  ERRORS_FUNC_END2( ERRORS_FUNC_INF, func );   /* 関数の終わり */
errors.h をインクルードする前に ERRORS_FUNCLOG_LEVEL を定義 して、ソースごとの表示レベルを設定できます。
個別の関数を強制表示するときは、ERRORS_FUNC_START, ERRORS_FUNC_END の 第1引数を 0 に、非表示にするときは9 に一時的に書き換えます。

C++ 言語では、メンバ変数の関数ポインタを取得できないので、以下のように ダミー変数を用意します。
int  CDlg::OnInitDialog()
{
  ERRORS_FUNC_CPP_VAR( CMixerDlg_OnInitDialog );   /* ダミー変数宣言 */
  ERRORS_FUNC_START_CPP( CMixerDlg_OnInitDialog );

  ERRORS_FUNC_END_CPP( CMixerDlg_OnInitDialog );

  return  FALSE;
}
ERRORS_FUNC_VAR は、変数宣言をするマクロです。
ダミー変数の名前は、コールスタックを見るときに表示されるので、 クラス名とメンバ変数名が分かるようにします。
関数コール履歴に対応していない関数がコールの途中にあっても構いません。 (対応している関数のみ履歴をとります)

ERRORS_FUNC_START ERRORS_FUNC_END は、関数の始まりと終わりを 示しているだけで、そのマクロを使用したからといって、 errors モジュールを必ず使用しなければならないことはありません。 これらのマクロを上書きしたり、無効にしたりして対応できるからです。

8-1. トラブルシューティング


9. ◆ C++ のデストラクタで後始末処理を行うときの注意

C++ では、グローバル変数のデストラクタは exit 関数の呼び出しの 後で呼び出されます。このため、本モジュールの Errors_exit 関数による デフォルトのテストツールが各種後始末関数を呼び出す前に実行されて しまいます。これを避けるには #define ERRORS_NO_EXIT_CHK を 設定してください。

デストラクタ中でエラーが発生した場合、Errors_exit 関数に再び入り、 デストラクタが2度起動してしまうので、 以下のように2度目は後始末処理をしないようにしてください。

CSampleApp::~CSampleApp()
{
  static int  count = 0;

  count++;
  if ( count == 1 ) {
    Xxx_finish( m_Xxx );

    ERRORS_CHK_DEFAULT();
  }
}


10. ◆ プログラム終了ポイント(exitラベル)

exit によってプログラムが終了したときに再開する OS が 無いときは、アセンブラで exit というラベルの ダミー関数が必要になります。 スタートアップ・ルーチン内のプログラム終了処理の先頭に exit ラベルをつけておくと良いでしょう。 デバッガ上でプログラム実行するときは、exit ラベルに ブレークポイントをつけるといいでしょう。

コンパイラや OS が提供するスタートアップ・ルーチンを 変更したくないときは、halt 命令をアセンブラで任意の場所に記述し、 exit というラベルを付けます。 exit ラベルは公開(globl)してください。


11. ◆ 各コンパイラのプログラム強制終了時の動作

プログラムが強制終了したときの動作は次のようになります。

デバッガによっては、上記の命令を検出できないでハングアップしたような状態になる 場合があるので注意してください。


written by Masanori Toda from Apr.30.1999