Win32 でのヒープ破損。どのように見つけますか?
-
08-06-2019 - |
質問
私は取り組んでいます マルチスレッド化された ヒープを破損している C++ アプリケーション。この破損を特定するための通常のツールは適用できないようです。ソース コードの古いビルド (18 か月前) は最新のリリースと同じ動作を示すため、これは長い間存在していましたが、単に気付かなかっただけです。欠点としては、ソース デルタを使用してバグがいつ導入されたかを特定することができないことです。 たくさん リポジトリ内のコード変更の数。
クラッシュ動作のプロンプトは、このシステムでスループットを生成すること、つまり内部表現に書き込まれるデータのソケット転送です。アプリに定期的に例外を引き起こす一連のテストデータがあります(さまざまな場所、さまざまな原因 - ヒープ割り当ての失敗など)。ヒープの破損)。
この動作は、CPU パワーまたはメモリ帯域幅に関連しているようです。マシンのそれぞれの機能が多ければ多いほど、クラッシュしやすくなります。ハイパー スレッディング コアまたはデュアルコア コアを無効にすると、破損の発生率が減少します (ただし、完全には解消されません)。これはタイミング関連の問題を示唆しています。
ここで問題は次のとおりです。
軽量のデバッグ環境で実行される場合 (たとえば、 Visual Studio 98 / AKA MSVC6
) ヒープ破損はかなり簡単に再現できます。10 ~ 15 分経過すると、何かがひどく失敗して例外が発生します。 alloc;
洗練されたデバッグ環境 (Rational Purify、 VS2008/MSVC9
または Microsoft Application Verifier でさえも)、システムはメモリ速度制限になり、クラッシュしません (メモリ制限:CPUが上記を超えていない 50%
, 、ディスク ライトが点灯していません。プログラムは可能な限り高速で実行され、ボックスを消費しています。 1.3G
2G の RAM)。それで、 問題を再現できる (ただし原因は特定できない) か、原因または再現できない問題を特定できるかのどちらかを選択する必要があります。
次にどこに行くかについての私の現時点での最善の推測は次のとおりです。
- めちゃくちゃ不潔なボックスを入手します (現在の開発ボックスを置き換えるには:2GB RAM
E6550 Core2 Duo
);これにより、強力なデバッグ環境で実行しているときに誤動作を引き起こすクラッシュを再現できるようになります。または - 演算子を書き換える
new
そしてdelete
使用するVirtualAlloc
そしてVirtualProtect
使い終わったらすぐにメモリを読み取り専用としてマークします。下を走るMSVC6
そして、解放されたメモリに書き込みをしている悪者を OS に捕らえさせます。はい、これは絶望の兆候です。一体誰が書き換えるのかnew
そしてdelete
?!これでは Purify らの場合と同じくらい遅くなるのだろうか。
そして、いいえ:Purify 機器を組み込んだ状態での出荷はオプションではありません。
同僚が通り過ぎて、「スタック オーバーフロー?スタック オーバーフローが発生しているのですか?!?」
さて、次の質問です。 ヒープ破損者を特定するにはどうすればよいですか?
アップデート:バランスを取る new[]
そして delete[]
問題解決に向けてかなりの道のりを歩んできたようだ。アプリがクラッシュするまでに 15 分ではなく、約 2 時間かかります。まだそこにはありません。さらに何か提案はありますか?ヒープの破損が続いています。
アップデート:Visual Studio 2008 でのリリース ビルドの方が劇的に優れているようです。現在の疑惑は~にかかっています STL
同梱される実装 VS98
.
- 問題を再現します。
Dr Watson
さらなる分析に役立つダンプが生成されます。
それについてはメモしておきますが、ワトソン博士がつまずくのは事後であって、ヒープが踏みつけられているときではないのではないかと心配しています。
別の試行は使用している可能性があります
WinDebug
デバッグ ツールとしては非常に強力であると同時に軽量です。
現時点ではそれがうまくいきました、もう一度言います:何か問題が起こるまではあまり役に立ちません。行為中の破壊者を捕まえたい。
おそらく、これらのツールを使用すると、少なくとも問題を特定のコンポーネントに絞り込むことができます。
あまり希望は持てませんが、絶望的な時代が必要です...
また、プロジェクトのすべてのコンポーネントのランタイム ライブラリ設定が正しいかどうか (
C/C++ tab
, 、VS 6.0 プロジェクト設定のコード生成カテゴリ)?
いいえ、そうではありません。明日、数時間かけてワークスペース (ワークスペース内に 58 のプロジェクト) を調べ、すべてがコンパイルされ、適切なフラグを使用してリンクされていることを確認します。
アップデート:これには 30 秒かかりました。すべてのプロジェクトを選択します。
Settings
ダイアログでは、適切な設定を持たないプロジェクトが見つかるまで選択を解除します (すべてのプロジェクトには正しい設定がありました)。
解決
私の最初の選択肢は、次のような専用のヒープツールです。 ページヒープ.exe.
new と delete を書き直すと便利かもしれませんが、下位レベルのコードによってコミットされた alloc は捕捉されません。これが必要な場合は、迂回した方がよいでしょう。 low-level alloc API
■ Microsoft Detours を使用します。
また、次のような健全性チェックも行われます。ランタイム ライブラリが一致していることを確認します (リリースとデバッグ、マルチスレッド vs.シングルスレッド、DLL とstatic lib)、不正な削除 (たとえば、delete [] が使用されるべき場所での削除) を探し、alloc が混在して一致していないことを確認してください。
また、スレッドを選択的にオフにして、問題がいつ解消されるかを確認してください。
最初の例外時のコールスタックなどはどのようになりますか?
他のヒント
私も仕事で同じ問題を抱えています(私たちも使っています) VC6
時々)。そしてそれに対する簡単な解決策はありません。いくつかのヒントしかありません。
- 実稼働マシンで自動クラッシュ ダンプを試してみます (「 プロセスダンパー)。私の経験では、博士はこう言います。ワトソンは 完璧ではない ダンプ用に。
- すべて削除する キャッチ(...) コードから。多くの場合、重大なメモリ例外が隠蔽されます。
- チェック 高度な Windows デバッグ - あなたのような問題に対する素晴らしいヒントがたくさんあります。これを心からお勧めします。
- 使用する場合
STL
試すSTLPort
そしてビルドを確認しました。無効なイテレータは地獄です。
幸運を。あなたのような問題は解決するまでに何か月もかかります。これに備えてください...
元のアプリケーションを実行します ADplus -crash -pn appnename.exe
メモリの問題が発生すると、大きなダンプが得られます。
ダンプを分析して、メモリのどの場所が破損したかを特定できます。運が良ければ、上書きメモリが一意の文字列である場合は、それがどこから来たのかを知ることができます。運が悪い場合は、掘り下げる必要があります win32
ヒープを作成して、元のメモリの特性が何であったかを把握します。(heap -x が役に立つかもしれません)
何が問題だったのかがわかったら、特別なヒープ設定を使用して appverifier の使用を制限できます。つまり何を指定できますか DLL
監視するか、監視する割り当てサイズ。
これにより監視が迅速化され、犯人を捕まえることができると期待されます。
私の経験上、フル ヒープ ベリファイア モードは必要ありませんでしたが、クラッシュ ダンプの分析とソースの参照に多くの時間を費やしました。
追伸:使用できます デバッグ診断 ダンプを分析します。指摘できるのは、 DLL
破損したヒープの所有者、その他の有用な詳細情報を提供します。
独自の malloc 関数と free 関数を作成することで、かなりの幸運を得ることができました。運用環境では、標準の malloc と free を呼び出すだけですが、デバッグでは、必要に応じて何でもできます。また、これらの関数を使用するために new 演算子と delete 演算子をオーバーライドするだけの単純な基本クラスもあり、作成したクラスはそのクラスから単純に継承できます。コードが大量にある場合、malloc と free の呼び出しを新しい malloc と free (realloc を忘れないでください) に置き換えるのは大変な作業になるかもしれませんが、長い目で見ると非常に役立ちます。
スティーブ・マグワイアの本の中で しっかりしたコードを書く (強くお勧めします)、これらのルーチンで実行できるデバッグ作業の例は次のとおりです。
- 割り当てを追跡してリークを見つける
- 必要以上のメモリを割り当て、メモリの先頭と末尾にマーカーを配置します。フリー ルーチン中に、これらのマーカーがまだ存在していることを確認できます。
- memset は、割り当て (初期化されていないメモリの使用量を見つけるため) と解放 (解放されたメモリの使用量を見つけるため) にマーカーを付けてメモリを設定します。
もう 1 つの良いアイデアは、 一度もない のようなものを使用します strcpy
, strcat
, 、 または sprintf
-- 常に使用する strncpy
, strncat
, 、 そして snprintf
. 。私たちはバッファーの終わりを書き込まないようにするために、これらの独自のバージョンも作成しましたが、これらでも多くの問題が発生しました。
この問題には、実行時分析と静的分析の両方を使用して対処する必要があります。
静的解析の場合は、PREfast でコンパイルすることを検討してください (cl.exe /analyze
)。不一致を検出します delete
そして delete[]
, 、バッファオーバーランやその他の多くの問題。ただし、特にプロジェクトにまだ問題がある場合は、何キロバイトもの L6 警告を乗り越える準備をしてください。 L4
未修理。
PREfast は Visual Studio Team System で使用できます。 どうやら, 、Windows SDKの一部として。
メモリ破損の明らかなランダム性は、スレッドの同期の問題によく似ています。マシンの速度に応じてバグが再現されます。オブジェクト (メモリのチャンク) がスレッド間で共有され、同期 (クリティカル セクション、ミューテックス、セマフォ、その他) プリミティブがクラスごと (オブジェクトごと、クラスごと) に基づいていない場合、次のような状況が発生する可能性があります。ここで、クラス (メモリのチャンク) は使用中に削除/解放されるか、削除/解放後に使用されます。
そのテストとして、各クラスとメソッドに同期プリミティブを追加できます。これにより、多くのオブジェクトが相互に待機する必要があるためコードが遅くなりますが、これによりヒープ破損が解消されれば、ヒープ破損の問題はコードの最適化の問題になります。
これはメモリ不足の状態ですか?もしそうなら、新しいものが戻ってくるかもしれません NULL
std::bad_alloc をスローするのではなく。古い VC++
コンパイラはこれを適切に実装していませんでした。についての記事があります 従来のメモリ割り当てエラー クラッシュする STL
で構築されたアプリ VC6
.
古いビルドを試しましたが、リポジトリ履歴をさらに遡ってバグがいつ発生したかを正確に確認できない理由はありますか?
それ以外の場合は、問題を追跡するために、ある種の単純なログを追加することをお勧めします。ただし、具体的に何をログに記録すればよいのかわかりません。
Google や発生している例外のドキュメントを参照して、この問題の正確な原因を知ることができれば、コード内で何を探すべきかについてさらに洞察が得られるかもしれません。
私の最初のアクションは次のようになります。
- 「リリース」バージョンでバイナリをビルドしますが、デバッグ情報ファイルを作成します (この可能性はプロジェクト設定で見つかります)。
- 問題を再現するマシン上で、ワトソン博士をデフォルトのデバッガ (DrWtsn32 -I) として使用します。
- 問題を再現します。ワトソン博士は、さらなる分析に役立つ可能性のあるダンプを作成します。
もう 1 つの試みは、非常に強力であると同時に軽量である WinDebug をデバッグ ツールとして使用することです。
おそらく、これらのツールを使用すると、少なくとも問題を特定のコンポーネントに絞り込むことができます。
また、プロジェクトのすべてのコンポーネントに正しいランタイム ライブラリ設定 (VS 6.0 プロジェクト設定の [C/C++] タブ、[コード生成] カテゴリ) があることを確認していますか?
したがって、限られた情報から、これは 1 つまたは複数の組み合わせである可能性があります。
- 不正なヒープ使用法、つまり、二重解放、解放後の読み取り、解放後の書き込み、同じヒープ上の複数のスレッドからの割り当ておよび解放による HEAP_NO_SERIALIZE フラグの設定
- メモリ不足
- 不正なコード (バッファ オーバーフロー、バッファ アンダーフローなど)
- 「タイミング」の問題
最初の 2 つだけで最後のものではない場合は、pageheap.exe のいずれかを使用してすでに検出しているはずです。
これは、コードが共有メモリにアクセスする方法が原因である可能性が高いことを意味します。残念ながら、それを追跡するのはかなり困難になるでしょう。共有メモリへの非同期アクセスは、奇妙な「タイミング」問題として現れることがよくあります。共有メモリへのアクセスをフラグと同期するために取得/解放セマンティクスを使用していないこと、ロックを適切に使用していないことなどです。
少なくとも、先に提案したように、何らかの方法で割り当てを追跡できると役立ちます。少なくとも、ヒープ破損に至るまでに実際に何が起こったのかを確認し、そこから診断を試みることができます。
また、割り当てを複数のヒープに簡単にリダイレクトできる場合は、それを試して、問題が解決するか、より再現可能なバグ動作が発生するかどうかを確認するとよいでしょう。
VS2008 でテストしていたとき、HeapVerifier で Conserve Memory を Yes に設定して実行しましたか?これにより、ヒープ アロケーターのパフォーマンスへの影響が軽減される可能性があります。(さらに、[デバッグ] -> [アプリケーション検証ツールで開始] を実行する必要がありますが、それはすでにご存知かもしれません。)
Windbg を使用したデバッグや、!heap コマンドのさまざまな使用法を試すこともできます。
MSN
新規/削除を書き直すことを選択した場合、私はこれを実行し、簡単なソースコードを次の場所に置きます。
http://gandolf.homelinux.org/~smhanov/blog/?id=10
これはメモリ リークを捕捉し、またメモリ ブロックの前後にガード データを挿入してヒープ破損を捕捉します。すべての CPP ファイルの先頭に #include "debug.h" を置き、DEBUG と DEBUG_MEM を定義するだけで、これと統合できます。
Graeme のカスタム malloc/free の提案は良いアイデアです。破損に関する何らかのパターンを特徴づけて、活用できるかどうかを確認してください。
たとえば、常に同じサイズ (たとえば 64 バイト) のブロック内にある場合は、独自のページに常に 64 バイトのチャンクを割り当てるように malloc/free ペアを変更します。64 バイトのチャンクを解放する場合は、そのページにメモリ保護ビットを設定して、読み取りと書き込みを防止します (VirtualQuery を使用)。その後、このメモリにアクセスしようとすると、ヒープが破損するのではなく、例外が生成されます。
これは、未処理の 64 バイト チャンクの数が中程度であること、またはボックスに書き込むメモリが大量にあることを前提としています。
似たような問題を解くのに少し時間がかかりました。問題がまだ存在する場合は、次のようにすることをお勧めします。new/delete および malloc/calloc/realloc/free へのすべての呼び出しを監視します。すべての呼び出しを登録するための関数をエクスポートする単一の DLL を作成します。この関数は、コード ソース、割り当てられた領域へのポインタ、およびこの情報をテーブルに保存する呼び出しのタイプを識別するためのパラメータを受け取ります。割り当てられた/解放されたペアはすべて削除されます。最後または必要になった後、残りのデータのレポートを作成するために他の関数を呼び出します。これにより、間違った呼び出し (new/free または malloc/delete) または欠落している呼び出しを識別できます。コード内でバッファが上書きされた場合、保存された情報は間違っている可能性がありますが、各テストで特定された障害の解決策が検出/発見/含まれる可能性があります。エラーを特定するために何度も実行します。幸運を。
これは競合状態だと思いますか?複数のスレッドが 1 つのヒープを共有していますか?HeapCreate を使用して各スレッドにプライベート ヒープを与えることができます。そうすれば、HEAP_NO_SERIALIZE を使用して高速に実行できます。それ以外の場合、システム ライブラリのマルチスレッド バージョンを使用している場合、ヒープはスレッド セーフである必要があります。
いくつかの提案。W4 での大量の警告について言及していますが、時間をかけて警告レベル 4 で正しくコンパイルできるようにコードを修正することをお勧めします。これは、見つけにくい微妙なバグを防ぐのに大いに役立ちます。
2 番目の /analyze スイッチの場合、実際に大量の警告が生成されます。このスイッチを自分のプロジェクトで使用するために、#pragma warning を使用して /analyze によって生成される追加の警告をすべてオフにする新しいヘッダー ファイルを作成しました。次に、ファイルのさらに下で、関心のある警告のみをオンにします。次に、/FI コンパイラ スイッチを使用して、このヘッダー ファイルがすべてのコンパイル単位に最初に組み込まれるように強制します。これにより、出力を制御しながら /analyze スイッチを使用できるようになります。