- 締切済み
VC++でCatchできる例外について
DBサーバにて常駐動作させているプロセス(VS6.0にて作成したVC++のプログラム)にエラーが発生して停止してしまいました。 イベントログにてアプリケーションログ、システムログを見ると、以下のようなエラーが出ていました。(日付、ユーザ、マシン名、エラー発生アプリケーション名は伏せております) ■アプリケーションログ■ イベントの種類: エラー イベント ソース: Application Error イベント カテゴリ: (100) イベント ID: 1000 説明: エラー発生アプリケーション <プログラム名>.exe、バージョン 0.0.0.0、エラー発生モジュール MSVCRTD.DLL、バージョン 6.0.8168.0、エラー発生アドレス 0x00011920 ■システムログ■ イベントの種類: 情報 イベント ソース: Application Popup イベント カテゴリ: なし イベント ID: 26 説明: アプリケーション ポップアップ: <プログラム名>.exe - アプリケーション エラー : 例外 unknown software exception (0x80000003) がアプリケーションの 0x10211920 で発生しました。 原因が分からないため、せめて上記エラーが発生しても停止しないよう、主要処理部全体を以下のように例外処理で囲むことで暫定対応しようとしております。 try{ 主要処理 }catch(...){ エラー発生時の処理 } 改修後、同様なエラーを模擬的に発生させ、停止しないことを確認しようとしましたが、「エラー発生モジュール MSVCRTD.DLL」となるエラーを発生させることができません。 上記try-catchで「エラー発生モジュール MSVCRTD.DLL」となるエラーがキャッチできるかどうか、ご存知の方はおられましたらご教示いただけないでしょうか? 宜しくお願い致します。
- みんなの回答 (5)
- 専門家の回答
みんなの回答
- chie65536(@chie65535)
- ベストアンサー率44% (8740/19838)
MSVCRTD.DLLは「デバッグ版マルチスレッド用CライブラリDLL版」です。 これが例外を吐くとしたら「例外の発生が規定されたライブラリ関数を、例外が起きる条件で呼び出した」か「ブレークポイントに出会った」か、どちらかです。 で、インテル系32/64ビットCPU用にコンパイルすると、関数が4または8または16バイト境界から始まるようにコード生成されるので「関数と関数の間に数バイトの隙間」が出来ます。 通常、この「関数と関数の隙間」の部分を実行する事は有り得ないので、この隙間にはブレークポイントを示す「INT 3」の命令コードが埋められています。 そして「もし、この隙間の部分にジャンプして来て、コードが存在しない部分を実行しようとしたら、ブレークポイントに出会い、例外が起きる」ようになっています。 もし、質問者さんのプログラムが「どこかで、ブレークポイントに出会った」としたら、考えられるのは、以下のケースです。 1.特権モードで実行中に、不正なポインタ操作でコード領域を破壊した(普通は発生しません。通常、コード領域はライトプロテクトされていて、書き込もうとするとライトプロテクト例外が発生します) 2.特権モードで実行中に、不正なポインタ操作でシステム領域や静的データ領域にあるジャンプテーブルを破壊し、不正なアドレスにジャンプした(普通は発生しません。通常、システム領域や静的データ領域はライトプロテクトされていて、書き込もうとするとライトプロテクト例外が発生します) 3.auto変数で定義した配列の要素数をオーバーしてアクセスし、スタック上に積まれた「関数からのリターン先」を破壊した。 これが「最も有力な原因」でしょう。 インテル系32ビットCPU用では、スタック上のメモリ・イメージは以下のようになっています。 例)引数が3つある関数で、256バイトのauto変数の配列を確保した場合 +00000000 auto変数256バイト +00000100 関数に入る際のBPレジスタ +00000104 関数から帰る先、リターンアドレス +00000108 1番目の引数 +0000010c 2番目の引数 +00000110 3番目の引数 +00000114 1つ上の親の関数のauto変数 +00000??? 1つ上の親の関数に入る際のBPレジスタ +00000??? 1つ上の親の関数から帰る先、リターンアドレス もし「auto変数の配列が256バイトしかない」のに、256バイトを超えてアクセスし、配列をはみ出して書き込むと「関数から帰る先、リターンアドレス」を書き替える事になります。 すると「関数を抜けた瞬間、どこか訳の判らないアドレスにジャンプ」します。 そのジャンプした先の「どこか訳の判らないアドレス」が「関数と関数の隙間」だった場合、CPUは「INT 3」に出会うので、結果的に「ブレークポイントに出会う」ことになります。 たぶん、プログラムのどこかに「配列の要素数を超えてアクセスしている部分」があります。それが「ブレークポイントに出会った原因」です。 今回は「吹っ飛んだ先に、ブレークポイントのINT 3が置いてある所だったため、例外が起きて止まった」ので運が良かったですが、もし、吹っ飛んだ先がどこかの関数の途中だったりすれば、無限ループしたり、重要なリソースやシステムデータを破壊したり、ファイルを削除したり、ファイルを書き替える可能性があります。 表面的に「例外をキャッチして停止しないようにするだけ」では「何の解決にもなっていない」ので、根本的なバグである「配列の要素数を超えてアクセスしている部分」を探し出して、バグを根治する必要があります。 「今回は、たまたま、運が良くて、例外で止まってくれたが、次は何が起きるか判らない」です。 多分、例外をキャッチするコードを追加すれば、命令コードの配置が変わります。 命令コードの配置が変われば、次から「INT 3に出会って例外で止まる保証は無くなる」ので、例外で拾えなくなったり、暴走したりするでしょう。 少なくとも「例外を拾っての対処療法」はすべきではありません。 転移先の癌をいくら切除しても無駄なのと同じで、癌の発生元を切除しないと、根本的な治療にはなりません。
MSVCRTD内で発生させるだけならあきらかにおかしなコードでも書けばいいんですよ #include <windows.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main (void) { PEXCEPTION_POINTERS Ex = NULL; __try{ strcpy (NULL, "qwerasdf"); } __except (Ex = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) { printf ("%p\n", Ex->ExceptionRecord->ExceptionAddress); } __try{ printf ("%s", 0x80000000); } __except (Ex = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) { printf ("%p\n", Ex->ExceptionRecord->ExceptionAddress); } __try{ div (0, 0); } __except (Ex = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) { printf ("%p\n", Ex->ExceptionRecord->ExceptionAddress); } }
お礼
ご回答頂き、ありがとうございました。 上記コードにて動作を確認させて頂きました。 やはりシステムログ上にアプリケーションポップアップのログが出るのみで、アプリケーションログには何も出ていませんでしたが、ご教示頂いたコードで発生するエラーはMSVCRTD内で発生しているのですね? /EHaオプション付きでコンパイルした状態で実施すると、try{}catch(...)としてもキャッチできることが確認できました。ありがとうございました。
- redfox63
- ベストアンサー率71% (1325/1856)
MSVCRTDならどこかのアサーションに引っかかったのかも ...
お礼
お礼申し上げるのが遅くなり、申し訳ありません。 ご意見、ありがとうございます。 MSVCRTD内でアサーションに引っかかった場合、 assert(0); というコードをテストプログラムにて実行した結果と同じになるのでしょうか?もし同じなら、上記コードの実行結果ではイベントログ(アプリケーション、システム共に)が出ていなかったので、本件で発生したエラーとは異なるかと思います。
- davidfox
- ベストアンサー率58% (21/36)
キャッチの可否は判りませんが、例外コードからするとブレークポイントに出会ったと言うことではないでしょうか。 VS6だと int WinMain(...){ _asm { int 3 }; return 0; } と書けば発生するはずです。 int 3 は0xccなのでデバッグ版DLLをお使いのようですから、間違って未初期化領域へ進んだとも考えられます。何とも言えません。 ハードの不良も0では無いと思いますし... また、デバッグ版は文字通りデバッグ以外に使えるかどうか怪しいのでは?
お礼
ご回答ありがとうございます。 上記コードのテストプログラムにてシステムログに関しては同様なログが表示することができました。その際、コンパイラオプションを/EHaとするとcatcy(...)にてキャッチできることも確認致しました。 問題発生時のシステムログにも「デバッグする場合は…」との表示がありましたので、davidfox様がおっしゃられるように「ブレークポイントに出会った」ということかと思います。 ご教示頂いたコードではアプリケーションログには何も表示されていなかったのですが、前述のとおりブレークポイントに出会ったのであればこれでキャッチできるかも…と期待しております。 デバッグ版DLLで動作していることについては私も気になっていました。 やはりデバッグ版でないバージョンへの変更も検討すべきかもしれないですね…。
VC++ではコンパイルオプションに/EHaを指定した場合に限り try catch(...)での構造化例外のキャッチが可能です 構造化例外処理(SEH)では全てのソフトウェア例外をキャッチ可能です(実行の継続が可能かは別として)
お礼
つまり、コンパイルオプションに「/EHa」を指定すると、C++例外、構造化例外と、発生しうる全ての例外をキャッチできるようになるということですね。ありがとうございます。 重ねて申し訳ありませんが、MSVCRTD.DLLで発生した「アプリケーションエラー」がこれらでキャッチ可能な例外に含まれるかどうかまではお分かりにならないでしょうか?
お礼
詳しい情報、ありがとうございます。 とても勉強になります。 ご教示頂いたように、テストプログラムにてauto変数の配列をはみ出して書き込んだ場合の動作を見てみました。 int test2() { int a[4]; a[4] = 100; } 上記のような関数をmainから呼び出すと、 「メモリがwrittenになりませんでした」 となり、a[5]=100; とすると、 「メモリがread」になりませんでした」となりました。 ちなみに、a[6]=100;とすると、エラーが発生することなく動作しました。 このテストプログラムではリターンアドレスが入っているであろうa[5]へは書き込みできませんでしたが、場合によっては書き込みができてしまうことがある、ということになるのでしょうか? 重ねて申し訳ありませんが、宜しくお願い致します。