アクセス違反(NULLポインタ)が発生したとしたら、
どのような手順・手法で原因追究しますか?
現在 Borland C++ Builder 6 Pro で
EAccessViolation (00000000番地のコードが00000000番地を参照)
というバグを修正したいのですが、発生場所が明確ではなく
こまっております。
当方の環境はBorland Delphiですが、開発環境としてはおそらく似たよう
な環境だと思いますのでご参考までに。
まずはエラーの発生場所を絞り込む必要がありそうですね。
毎回同じ場所でエラーが出るということであれば、そのエラーが起きる場所
に関連していそうなプログラムソース上で「ブレークポイント」というもの
を設定し、ステップ実行(1行ずつ実行)をすると良いと思います。
ブレークポイントを設定した状態でプログラムを起動すると、その設定した
箇所で動作が一時停止します(監視式を設定すれば変数の内容もチェックで
きます)
例えばエラーが起動後すぐに出るのであれば、プログラム起動後すぐに呼び
出される部分のソースにブレークポイントをかけて、ステップ実行で追っか
けると良いでしょう。
場所の特定さえ出来てしまえば、その部分に関連する変数や、関数などのチ
ェックを行うことで、おおよその原因はつかめるのではないでしょうか。
ブレークポイント、ステップ実行などの設定方法が分からない場合はC++buil
derのヘルプを参照すると良いと思います。
自分の場合を書きます。
まず、大前提として、デバッガ経由で実行しても原因箇所が特定されないということでしょうか?Borland C++ Builderのことはよく知りませんが、たいていのケースでは、これでエラーの箇所を特定してくれることが多いです。
自分の場合を書いておきます。はじめにソースを書く段階で、
#include<cassert>
して、assertマクロをあちこちに埋め込んでおきます。
具体的には、ポインタを受け取る関数の先頭で
assert( ptr );
をしておきます。これで、NULLでないはずのポインタがNULLになった場合に、異常終了して位置を教えてくれます。
せっかくC++を使うなら、boost::shared_ptrなどを使うのがお勧めです
C++Builderに付属のCodeGuardで
原因を探ることができると思います。
CodeGuardは、C++Builderの[ツール]メニューで
設定できます。使い方はヘルプで"CodeGuard"を
参照してみてください。
オブジェクトの生成/破棄の不整合などの発生箇所を
ソースコードの行番号つきでログ出力してくれます。
当方はBorland C++は使っておりませんが、C++言語全般で使えそうな方法を書きます。
予防策として基本的にポインタを使わないようにします。STLやBoost libraryにあるライブラリを使えばポインタを直接使わずに済むようになり、バグを減らすことができるかもしれません。
それでもポインタが必要なときはポインタを受け取るときやポインタにアクセスする前にassertによってポインタがNULLでないことをチェックするようにしておきます。ただし、assertは基本的にバグを見つけるためのもので、バグがない状態でもポインタがNULLになりえる場合はassertを使うべきではないと思います。
assertはマクロなのでデバッグ用にコンパイルしたときしか動作せず、それ以外のときは引数の式すら評価されません。
それでもNULLが発生したら、デバッガを使ってブレークポイントを設定したりポインタの値をチェックしたりして原因の箇所を探します。
さらにそれでも原因がわからない場合やデバッガを使えないときは、問題がありそうな箇所をコメントアウトして実行してみて、その場所にバグがあるかどうか確かめます。
問題がありそうな箇所が大量にある場合は、2分探索法みたいなやり方で効率良くバグを見つけます。
まず、問題がありそうなコードを半分に分けてどちらにバグがあるかどうか確かめます。
次にバグが含まれているとわかった方をさらに半分にしてどちらにバグがあるか確かめます。
これを問題箇所が見つかるまで繰り返します。
要するに問題の切り分けをするという話だと思います。assertは「ここまでは確実に正しい」ことを検査するものですし、2文探索みたいなのもそうです。私はCppUnitなどの単体テストを書いておくことをお勧めします。
単体テストで動作確認するようにしておけば、単体テストでバグがみつかれば場所は絞り込めているわけですし、単体テストでは動いて結合テストで失敗するなら結合の仕方が悪いと分かるわけです。
いざとなればテキストエディタのマクロなどでソースの全ての行間にassertを埋め込むなんて荒業もありますけど、いざというとき困らないように、あらかじめ問題の切り分けをしておく、というのがスマートだと思います。
プラットフォームや開発環境に依存しますが、(Borland C++ Builderでできるかどうかはわかりません)
これらの情報から基本的には追いかけることが可能です。
スタックトレースからは、アクセス違反が発生した状況での関数の呼び出し履歴が得られます。デバッグ版で無い場合は直アドレスしか得られない場合がありますが、mapファイルとアセンブリファイルから付き合わせることでアドレスから発生した関数名およびソース行を特定することができます。
アセンブリ言語の知識なども若干必要になってくるので最初は面倒ですが、1度やり方に慣れてしまえば原因箇所をズバリ特定できますので便利です。
比較的簡単に現象が再現できるようなら __try ~ __except でログを吐き出しつつ発生箇所を狭めていくという方法でも良いかもしれません。
コメント(1件)
当方で発生したバグは、みなさんの回答をもとに捜した結果、
みつけることができました。ありがとうございます。
今回みなさんの技の数々を聞いて、とても興味をもちました。
ということから、この質問は時間いっぱいまで継続させていただきます。
なお、1〜5回答者の方には、問題解決をお手伝い頂きましたので
少し大目にpt進呈いたします。また、今後ご解答頂ける方でも
技法に応じてptを進呈する予定ですので、よろしくお願い致します。