Visual C++ 2008 MFCです。
現在開発中のアプリケーションで、
不定期に発生するエラーの原因究明に手こずって困っています。
発生しないときは、10分以上操作しても発生しないのですが、
いざ発生すると、
*.exe の 0x7c968711 で初回の例外が発生しました: 0xC0000005: 場所 0x00000008
を読み込み中にアクセス違反が発生しました。
と表示されたり、
HEAP[*.exe]: HEAP: Free Heap block 1a0250 modified at 1a02e0 after it was freed
Windows によって *.exe でブレークポイントが発生しました。
ヒープが壊れていることが原因として考えられます。*.exe または読み込まれた DLL
にバグがあります。
と表示されたりと、いろんなケースで止まってしまいます。
そのときの呼び出し履歴を見てみても、「ntdll.dll」の中だったり、
最後に自分自身で呼んだ関数はただのSendMessage()だったりで、
なぜそんなところでという場所です。
そのエラー内容から、どこかで解放済みのメモリを
書き換えてしまっているんだろうと思っているのですが、
この原因を突き止めるためには、どのような追跡方法がありますでしょうか?
不定期に発生するので、あちこちにブレークポイントを置くわけにもいきませんし、
止まったしまったあとはメモリはすでに書き換わってしまっていますし、
毎回書き換わるアドレスも違うようなので、
そのアドレスを使ってブレークポイントを作っておくのも難しいです。
_CrtCheckMemory
_CrtSetDbgFlag
とかかな。
でも、これでつかってじっくり調べてわかるのは同じ動作をさせた場合に、
同じ場所で毎回発生する場合。
いろんなケースで止まるとなると、特定できるかどうかわからない。
●どんな内容のソフト作ってるのか?
(毎回動作が異なるものなのかどうか?とか、外部デバイスつかってるかとか?)
●それを今までどんなふうに作ってきたのか?
(vc2008で新規プロジェクトで作り始めたのか?とか)
●テストはどんなふうにやってるのか?
(メモリやオブジェクトの監視、別のパソコンでも同じ症状がおきるかどうか?とか)
など、情報が出せるでしょうか?
んー,これはクイズかな?
開放したメモリ領域にアクセスをしようとしたっぽいので,
freeとかdelete周辺が怪しいよね
明示的なfreeやdeleteがあるならもう調べてるはずだと仮定して,
では,意図しない時に開放が呼ばれてるのかな?
とすると,コピーコンストラクタが呼ばれて生成されたオブジェクトのデストラクタなん
かどうだろ?
メンバにポインタ変数を持つもので,
かつデストラクタでそれの開放をしている振る舞いのあるクラスの
コピーコンストラクタ!
どうかな?
だとしたら,引数に参照でもなく,ポインタでもないクラスを引数として取っているメソ
ッドがあるかもね
P.S.
> 不定期に発生するので、あちこちにブレークポイントを置くわけにもいきませんし、
>
作っているものが商品ならば,解決させるのは責任だと思うので,
あちこちにブレークポイントを置くわけにもいきませんなんて言ってられないので,
私なら,迷わず止めまくるかも...www
「0xC0000005」はEXCEPTION ACCESS VIOLATIONと呼ばれプログラム上の
メモリアクセス違反を意味します。
「場所 0x00000008」はそのとき読み書きしようとしたポインタの値です。
>そのエラー内容から、どこかで解放済みのメモリを
>書き換えてしまっているんだろうと思っているのですが、
それにこだわらないほうが吉かもしれません。その他、考えられるものとして
1.あるクラスを、別のクラスとしてdeleteした場合等、ポインタの読み替え間違い。
(特定の場所で起きやすいが、そうでない場合もありえる。Heapエラーになりやすい)
2.配列などのオーバーランによるとばっちり。
(まったく関連の無い場所に影響が出る場合が多いが、特定のエラーを繰り返す
こともある)
さて、幸運にもデバッグビルドで発生するならば次のことを覚えておくと吉。
1.スタックに用意された未初期化変数は0xccccに初期化されている。
(これにアクセスするとコンパイル時に警告されますけどね)
2.未使用又はfree delete されたヒープメモリは0xfeeeに初期化されている。
つまり「場所 0xfeeefeee」だったらビンコ゜
その他、0xcdcd 0xdddd 0xfdfd 0xabab 0xbaadfood 等は、VCが独自に
メモリーを初期化した結果で、それぞれが別の意味を持ってます。
興味があったら調べてみましょう(^^;)。
× 0xbaadfood
○ 0xbaadf00d
・・・orz.
いよいよ解らなければ、ソースレベルで見直しですかねぇ。
冗談ではなくて実際に同じようなケースで全ソース見直しなんて事に
なった例もありますよ。
場所を特定できる情報が得られないのであれば、ローラー作戦で
一度潰してしまうと言うのも手段の一つです。
効率云々の話をするとかなり厳しい物が有りますけれど。
後は、高価なソフトになりますけれどピューリファイ等の
メモリー関連の不具合検出ソフトを使うかと言う話になります。
それなりに高価なソフトになるので簡単に導入と言うわけには
いかないと思いますが、全く検討がつかない状態ならそれも
ありなのかもと思います。
メモリー関連の不具合はデバッグビルドではなくて
リリースビルドでは出るとか、特定の環境だと出るのに
とか色々あるのでけっこう難しいんですよね。
デバッグビルドで出ていれば万歳ですね。
追いかけるネタをある程度はデバッガが出してくれますから。
ある程度、場所を絞れるなら怪しい所にOutputDebugStringを
入れて、デバッグモニター系のソフトで監視かなぁ。
OutputDebugStringはTRACEマクロの中で呼ばれている関数で
直接呼べば、リリース版のソフト上でも使えます。
出力された情報はデバッグモニター系のソフトで見る事が出来ます。
ただし、メモリ関連のバグはワンステップ追加しただけで
挙動が変わったりするので挙動だけに捕われすぎると追えなくなるので
ある程度は試行錯誤する必要があると思いますよ。
確実に追い込めるツールと言うのは多分無いと思います。
ryoさん、hiroccoさん、仲澤@失業者さん、PATIOさん、
ご意見ありがとうございます。
皆さんからいただいた意見を元にいろいろな方法で試しているところですが、
まだ問題は解決しておりません。
ちなみに、以下の件について、現状でわかっていることを追記させていただきます。
HEAP[*.exe]: HEAP: Free Heap block 24c788 modified at 24c818 after it was freed
Windows によって *.exe でブレークポイントが発生しました。
という出力が運良く出た場合の、その付近のメモリ内容を見てみると、
0x0024C78C 5c 04 18 00 08 80 9d 04 58 02 16 00 ee fe ee fe ee fe ee fe
0x0024C7A0 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C7B4 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C7C8 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C7DC ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C7F0 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C804 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C818 80 00 00 00 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C82C ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C840 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C854 ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe ee fe
0x0024C868 05 00 1c 00 a0 07 18 00 00 00 00 00 a0 2b 25 00 00 00 00 00
となっています。
0x00000008 を読み込み中にアクセス違反が発生しました
というエラーも、たしかに0x0024C818番地に0x00000008が書かれていて、
しかも周りが0xfeeefeeeとなっているので、解放されたところにアクセスに行って、
そこに書かれている値をアドレスだと解釈しているということになりますよね。
ただ、この0x0024C818という番地自体は毎回違う値なので、
「このアドレスが書き換わったら」というようなブレークは用意できず、
ヒープエラーが出ずにいきなり0xC0000005のエラーが出力されることもあります。
newで確保したメモリがdeleteされていない場合には
デバッグ終了時にダンプしてくれますが、
その逆(上記のような上書き)では、その瞬間には停止してくれないのでしょうか。
トレースしてくれるのならそこで止めてくれよと思ってしまうのですが。
ロードローラーだッ!! はさておき
class class_hoge
{
class_hoge();
public:
virtual ~class_hoge();
static class_hoge* new_hoge()
{
return new class_hoge;
}
void delete_hoge()
{
try
{
delete this;
}
catch(...)
{
TRACE(double delete.);//ログ
}
}
};
単なる多重開放だとこんな風に書けなくもない。もっともここで改めて発言するほど
たいしたレベルじゃないです自分
やはり困るのは配列オーバーランのとばっちりでしょうかね
boost::arrayに書き換えてデバッグビルド版で提供してしまうとか
メモリ関連はどんなパターンでも考えられるので一括りで
話が出来ないんですよねぇ。
例えば、とある配列のオーバーフローが原因でその後にあるポインタ変数を
ぶっ壊していたとすると、そのポインタ変数が指している先はたまたま確保している
メモリ内だったりしてそのメモリ内の値を何かのポインタとして使っていたら
もうその時々によってぶっ飛ぶアドレスは変わっちゃうわけです。
こういうケースがすごい綱渡り状態で発生していたりすると
状況によって何処までたどった所で落っこちるのかも変わってしまうので
単純に追いかけただけじゃ駄目よとなります。
良く話しているんですが、基本的にメモリ破壊系のバグは何が起こっても
おかしく無いという話をしています。
それだけに厄介なんですけれども。
ちなみにローカル変数にポインタがあって
これが壊されている事が引き金になっている場合
ポインタの値の破壊でデバッガが何か出してくれるかと言うと
出してくれません。出してくれるのはヒープみたいに動的に
確保/開放をするケースのはずです。
なので、本当の原因がローカル変数の破壊にある場合は、
デバッガの出力メッセージだけを頼りにしても分からないかもしれません。
最初の破壊が直接の原因になって落ちているケースなら
追いかける事も可能かもしれませんけれど、複雑なケースになると
難しいと思います。
ビンゴなケースを捕まえられると何とかなるかもですが。
市販のデバッグツールなどを試してみたり、
ソースレベルで深く追っかけてみたりしたところ、
どうやらこれだという箇所が見つかりました。
わかってしまうと恥ずかしい話なのですが、他の人が作った汎用DLL内に、
GlobalAlloc()やGlobalFree()やGlobalLock()に使うハンドルなどを
グローバル領域上の静的変数で持ち続けている箇所があり、
それに対する操作関数を、こちらで作っているアプリケーションで
複数のスレッドから呼んでいました。
汎用DLLのほうをスレッドセーフになるように作り替えてみてもらったところ、
今回の問題は現在のところは発生しなくなりました。
本当に解決しているのか、それとも修正によってたまたまメモリ上の並びが変わって
うまいこと発生しにくくなっただけで、まだ他の場所にもバグが潜在しているのか、
そこまでは判断することはできませんが、
この手の問題はデバッガだけでは簡単につかめないものなのですね。
皆さんありがとうございます。