メモリ破損のデバッグ


23

まず最初に、これは絶対的な答えのある完璧なQ&Aスタイルの質問ではないことを理解していますが、それを改善するための表現は考えられません。これに対する絶対的な解決策はないと思います。これが、Stack Overflowではなくここに投稿する理由の1つです。

先月、私はかなり古いサーバーコード(mmorpg)をより近代的で拡張/修正しやすいものに書き換えました。私はネットワーク部分から始め、サードパーティのライブラリ(libevent)を実装して、自分のものを処理しました。すべてのリファクタリングとコードの変更により、どこかでメモリ破損が発生し、どこで発生するかを見つけるのに苦労しています。

原始的なボットを実装して負荷をシミュレートしても、クラッシュが発生しない場合でも、開発/テスト環境で確実に再現することはできません(何らかの原因で発生したlibeventの問題を修正しました)

私はこれまで試しました:

地獄を破壊する-ものがクラッシュするまで無効な書き込みはありません(実稼働では1日以上かかる場合があります。1時間かかる場合があります)。これは本当に私を困惑させます。チャンス?(アドレス範囲を「広げる」方法はありますか?)

コード分​​析ツール、すなわちコベリティとcppcheck。彼らはいくつかの..を指摘しましたが、コードの厄介さとエッジケースは深刻なものではありませんでした。

(undodbを介して)gdbでクラッシュするまでプロセスを記録し、逆方向に作業します。この/ sounds /は実行可能であるはずですが、オートコンプリート機能を使用してgdbをクラッシュさせるか、可能性のあるブランチが多すぎるために失われる内部libevent構造になります(1つの破損が原因でに)。ポインターが元々どの場所に/どこに割り当てられていたのかを見ることができれば、ブランチの問題のほとんどを解消できると思います。ただし、undodbを使用してvalgrindを実行することはできません。通常のgdbレコードは、使用できないほど遅くなります(valgrindと組み合わせても機能する場合)。

コードレビュー!自分で(完全に)そして何人かの友人に私のコードを見てもらうことで、それが十分に徹底的だったとは思いませんが。コードレビュー/デバッグを行うために開発者を雇うことを考えていましたが、多額の資金を投入する余裕はありません。彼が問題を見つけられなかったり、資格を持っている人がいなければ、お金はありません。

また、注意する必要があります。通常、一貫したバックトレースが取得されます。クラッシュが発生する場所はいくつかありますが、大部分は何らかの理由でソケットクラスが破損することに関連しています。ソケットではない何かを指している無効なポインタか、ソケットクラス自体が(部分的に)意味不明なもので上書きされています。よく使用される部品の1つであるため、クラッシュが最も多いと思われますが、使用されるのは最初に破損したメモリです。

全体として、この問題はほぼ2か月間(忙しく、趣味のプロジェクトが多い)忙しく、不機嫌なIRLになり、あきらめることを考えるほどイライラしています。私は問題を見つけるために他に何をすべきかについて考えることができません。

見逃した便利なテクニックはありますか?どのように対処しますか?(これについてはあまり情報がないため、それほど一般的ではありません..または私は本当に盲目ですか?)

編集:

重要な場合の仕様:

gcc 4.7を介したc ++(11)の使用(debian wheezyが提供するバージョン)

コードベースは約15万行です

david.pfx投稿への応答で編集:(応答が遅くなって申し訳ありません)

パターンを探すために、クラッシュの注意深い記録を保持していますか?

はい、私はまだ周りの最近のクラッシュのダンプを持っています

いくつかの場所は本当に似ていますか?どのように?

さて、最新バージョン(コードを追加/削除したり、関連する構造を変更するたびに変更されるようです)では、常にアイテムタイマーメソッドでキャッチされます。基本的に、アイテムには期限が切れる特定の時間があり、更新された情報をクライアントに送信します。無効なソケットポインタは、主にそれに関連するPlayerクラス(私の知る限り有効)にあります。また、クリーンアップフェーズで、明示的に破棄されていないすべての静的クラスを(__run_exit_handlersバックトレースで)破棄する通常のシャットダウンの後、クラッシュの負荷が発生しています。ほとんどの場合std::map、1つのクラスが関与しますが、それが最初に現れるのは単なる推測です。

破損したデータはどのように見えますか?ゼロ?アスキー?パターン?

まだパターンが見つかりませんでした。破損がどこから始まったのかわからないので、わかりにくいです。

ヒープ関連ですか?

それは完全にヒープ関連です(gccのスタックガードを有効にしましたが、何もキャッチしませんでした)。

破損は後に発生しfree()ますか?

少し詳しく説明する必要があります。すでに解放されたオブジェクトのポインターが横になっているということですか?オブジェクトが破棄されると、すべての参照をnullに設定するので、どこかで見逃さない限り、いいえ。valgrindには表示されますが、表示されませんでした。

ネットワークトラフィック(バッファサイズ、リカバリサイクル)に特有のものはありますか?

ネットワークトラフィックは生データで構成されます。したがって、char配列、(u)intX_tまたはより複雑なもののための(パディングを削除するための)パックされた構造体には、各パケットに、予想されるサイズに対して検証されるidおよびパケットサイズ自体で構成されるヘッダーがあります。サイズは10〜60バイトで、最大の(内部「起動」パケット、起動時に1回起動される)サイズは数Mbです。

多くの生産が主張します。損傷が伝播する前に、早期かつ予想どおりにクラッシュします。

私はかつてstd::map破損に関連したクラッシュを経験しました。各エンティティには「ビュー」のマップがあり、それを見ることができる各エンティティはその中にあります。前後に200バイトのバッファーを追加し、0x33で埋め、各アクセスの前にチェックしました。腐敗は魔法のように消え去りました。私は何かを動かして、他の何かを腐敗させたに違いありません。

戦略的なロギング。これにより、直前に何が起こっていたかを正確に把握できます。回答に近づいたら、ログに追加してください。

それは機能します。

必死になって、状態を保存して自動再起動できますか?私はそれを行う生産ソフトウェアのいくつかの部分を考えることができます。

やややる。このソフトウェアは、メインの「キャッシュ」プロセスと、すべてのものを取得して保存するためにすべてキャッシュにアクセスする他のワーカープロセスで構成されています。そのため、クラッシュごとに大きな進歩を失うことはありません。それでもすべてのユーザーが切断されるなど、間違いなく解決策ではありません。

並行性:スレッド化、競合状態など

「非同期」クエリを実行するmysqlスレッドがありますが、これはすべてそのままで、すべてのロックを備えた関数を介してデータベースクラスと情報を共有するだけです。

割り込み

30秒間のサイクルを完了しなかった場合に停止するロックを防ぐための割り込みタイマーがありますが、そのコードは安全なはずです:

if (!tics) {
    abort();
} else
    tics = 0;

ティックはvolatile int tics = 0;、サイクルが完了するたびに増加します。古いコードも。

イベント/コールバック/例外:状態またはスタックが予期せず破損する

多くのコールバックが使用されています(非同期ネットワークI / O、タイマー)が、悪いことは何もすべきではありません。

異常なデータ:異常な入力データ/タイミング/状態

それに関連したいくつかのエッジケースがありました。パケットの処理中にソケットを切断するとnullptrなどにアクセスすることになりますが、クラス自体に完了を通知した直後にすべての参照がクリーンアップされるため、これまでのところ簡単に見つけることができました。(破壊自体は、サイクルごとに破壊されたオブジェクトをすべて削除するループによって処理されます)

非同期外部プロセスへの依存。

手入れをしますか?これは、上記のキャッシュプロセスの場合です。頭上で想像できるのは、十分な速さで完了せず、ガベージデータを使用することだけですが、それもそうではありません。ネットワークを使用しているからです。同じパケットモデル。


7
悲しいことに、これは重要なC ++アプリでよく見られます。ソース管理を使用している場合、さまざまな変更セットをテストして問題の原因となったコード変更を絞り込むことができますが、この場合は実行不可能な場合があります。
テラスティン14

ええ、それは本当に私の場合は実行不可能です。私は基本的に作業から完全に完全に壊れて2か月間壊れてから、ある程度動作するコードがあるデバッグ段階に行きました。古いシステムでは、すべてを壊さずに、新しい柔軟なネットワークコードを実装することはできませんでした。
ロビン14

2
この時点で、各部分を分離する必要があります。ソリューションの各クラス/サブセットを取得し、それが機能するようにモックを作成し、失敗するセクションが見つかるまで、それから生きている地獄をテストします。
アンプト14

クラッシュが発生しなくなるまで、コードの一部をコメントアウトすることから始めます。
cpp81 14

1
Valgrind、Coverity、およびcppcheckに加えて、AsanとUBsanをテスト体制に追加する必要があります。コードがcorss-platofrmである場合は、Microsoftのエンタープライズ分析(/analyze)、AppleのMallocおよびScribbleガードも追加します。また、コンパイラの警告は診断であ​​り、時間とともに改善されるため、できるだけ多くの標準を使用して、できるだけ多くのコンパイラを使用する必要があります。特効薬はなく、1つのサイズがすべてに適合するわけではありません。使用するツールとコンパイラが多いほど、各ツールには長所と短所があるため、カバレッジはより完全になります。

回答:


21

それは難しい問題ですが、すでに見たクラッシュにはもっと多くの手がかりがあると思います。

  • パターンを探すために、クラッシュの注意深い記録を保持していますか?
  • いくつかの場所は本当に似ていますか?どのように?
  • 破損したデータはどのように見えますか?ゼロ?アスキー?パターン?
  • 関連するマルチスレッドはありますか?競合状態になる可能性がありますか?
  • ヒープ関連ですか?free()の後に破損が発生しますか?
  • スタック関連ですか?スタックは破損しますか?
  • ぶら下がり参照は可能ですか?神秘的に変化したデータ値?
  • ネットワークトラフィック(バッファサイズ、リカバリサイクル)に特有のものはありますか?

同様の状況で使用したもの。

  • 多くの生産が主張します。損傷が伝播する前に、早期かつ予想どおりにクラッシュします。
  • たくさんの警備員。ローカル変数、オブジェクト、およびmallocs()の前後に追加のデータ項目が値に設定され、頻繁にチェックされます。
  • 戦略的なロギング。これにより、直前に何が起こっていたかを正確に把握できます。回答に近づいたら、ログに追加してください。

必死になって、状態を保存して自動再起動できますか?私はそれを行う生産ソフトウェアのいくつかの部分を考えることができます。

お手伝いできる場合は、詳細を自由に追加してください。


このような深刻な不確定なバグはそれほど一般的ではなく、(通常)それらを引き起こす可能性のあるものは多くありません。彼らが含まれます:

  • 並行性:スレッド化、競合状態など
  • 割り込み/イベント/コールバック/例外:状態またはスタックが予期せず破損する
  • 異常なデータ:異常な入力データ/タイミング/状態
  • 非同期外部プロセスへの依存。

これらは、焦点を当てるコードの部分です。


+1すべての良い提案、特にアサーション、ガード、ロギング。
andy256

私はあなたの答えへの応答として私の質問のいくつかの情報を編集しました。それは実際に私がまだ広範囲に見ていないシャットダウン時のクラッシュについて考えさせられたので、私は今のところそれを得るつもりだと思います。
ロビン14

5

malloc / freeのデバッグバージョンを使用します。それらをラップし、必要に応じて独自に作成します。たくさんの楽しみ!

私が使用するバージョンは、すべての割り当ての前後にガードバイトを追加し、解放されたチャンクをチェックする「割り当てられた」リストを維持します。これは、ほとんどのバッファオーバーランと、複数または不正な「フリー」エラーをキャッチします。

最もinな汚職の原因の1つは、解放された後もチャンクを使用し続けることです。Freeは、解放されたメモリを既知のパターン(従来は0xDEADBEEF)で埋める必要があります。割り当てられた構造に「マジックナンバー」要素が含まれ、構造を使用する前に適切なマジックナンバーのチェックを自由に含める場合に役立ちます。


1
ただし、Valgrindは二重の解放/解放されたデータの使用をキャッチする必要があります。
ロビン14

新規/削除のためにこの種のオーバーロードを記述することで、多数のメモリ破損の問題を見つけることができました。特に、削除時に検証され、プログラムがブレークポイントをトリガーするガードバイトは、自動的にデバッガーにドロップされます。
エミリーL.

3

あなたの質問であなたが言うことを言い換えると、あなたに決定的な答えを与えることは不可能です。私たちができる最善のことは、探すべきものやツールやテクニックを提案することです。

いくつかの提案は素朴に見えますが、他の提案はより適切に縫い合わされるかもしれませんが、うまくいけば、あなたがフォローアップできる考えを引き起こします。david.pfxによる答えには、適切なアドバイスと提案があると言わなければなりません。

症状から

  • 私には、バッファオーバーランのように聞こえます。

  • 関連する問題は、未検証のソケットデータを添え字またはキーなどとして使用していることです。

  • どこかでグローバル変数を使用しているのか、同じ名前のグローバル変数とローカル変数があるのか​​、あるプレイヤーのデータが別のプレイヤーのデータに干渉する可能性はありますか?

多くのバグと同様に、おそらくどこかで無効な仮定をしていることになります。または場合によっては複数。複数の相互作用するエラーを検出するのは困難です。

  • すべての変数に説明がありますか?また、有効性アサーションを定義できますか?
    これらを追加しない場合は、コードをスキャンして、各変数が正しく使用されているように見えることを確認します。理にかなっているところならどこでも、その主張を追加してください。

  • ロットアサーションを追加する提案は良いものです。それらを配置する最初の場所は、すべての関数エントリポイントにあります。引数および関連するグローバル状態を検証します。

  • 長時間実行/非同期/リアルタイムのコードをデバッグするために、多くのログを使用します。
    繰り返しますが、関数呼び出しごとにログ書き込みを挿入します。
    ログファイルが大きくなりすぎると、ログ関数がファイルをラップ/切り替え/などする可能性があります。
    ログメッセージが関数呼び出しの深さでインデントされている場合に最も役立ちます。
    ログファイルは、バグの伝播方法を示すことができます。遅延アクション爆弾として機能する非常に適切でないコードを1つのコードが実行する場合に役立ちます。

多くの人は、自分で作成した独自のログコードを持っています。私はどこかに古いCマクロログシステムを持っています、そしておそらくC ++バージョンが...


3

他の回答で述べられたことはすべて非常に重要です。ddyerが部分的に言及している重要なことの1つは、malloc / freeのラップには利点があるということです。彼はいくつか言及していますが、それに非常に重要なデバッグツールを追加したいと思います:すべてのmalloc / freeを数行のコールスタック(または気にする場合は完全なコールスタック)とともに外部ファイルに記録できます。慎重にすれば、これを簡単に非常に高速に作成して、本番環境で使用することができます。

あなたが説明していることから、私の個人的な推測では、あなたはどこかにポインタを解放したメモリへの参照を保持しているかもしれず、もはやあなたに属していないか、それを書いているポインタを解放するかもしれません 上記の手法で監視するサイズ範囲を推測できる場合は、ログを大幅に絞り込むことができるはずです。それ以外の場合、破損したメモリを見つけると、ログから非常に簡単にそれを導いたmalloc / freeパターンを見つけることができます。

重要な注意点は、言及したように、メモリレイアウトを変更すると問題が隠れる可能性があることです。したがって、ロギングが割り当てを行わない(可能な場合)か、できる限り少なくすることが非常に重要です。これは、メモリに関連する場合の再現性に役立ちます。また、問題がマルチスレッドに関連している場合、可能な限り高速であれば役立ちます。

また、サードパーティのライブラリからの割り当てをトラップして、適切にログに記録できるようにすることも重要です。それがどこから来たのか、あなたは決して知りません。

最後の選択肢として、すべての割り当てに少なくとも2ページを割り当て、解放時にそれらのマッピングを解除するカスタムアロケーターを作成することもできます(allocをページ境界に揃え、前にページを割り当て、アクセス不可としてマークするか、ページの最後に割り当て、後にページを割り当て、マークはアクセス不可として)。少なくともしばらくの間、これらの仮想メモリアドレスを新しい割り当てに再利用しないようにしてください。これは、仮想メモリを自分で管理する必要があることを意味します(必要に応じて仮想メモリを確保して使用します)。これによりパフォーマンスが低下し、フィードの割り当て数に応じてかなりの量の仮想メモリが使用されることに注意してください。これを緩和するために、64ビットで実行できる場合や、これを必要とする割り当ての範囲を(サイズに基づいて)削減できる場合に役立ちます。Valgrindは既にこれを実行している可能性がありますが、問題をキャッチするには遅すぎる可能性があります。いくつかのサイズまたはオブジェクトに対してのみこれを行うと(これらのオブジェクトにのみ特別なアロケーターを使用できる場合)、パフォーマンスへの影響を最小限に抑えることができます。


0

クラッシュするメモリアドレスにウォッチポイントを設定してみてください。GDBは、無効なメモリの原因となった命令でブレークします。次に、バックトレースを使用して、破損の原因となっているコードを確認できます。これは破損の原因ではないかもしれませんが、各破損で監視ポイントを繰り返すと、問題の原因につながる可能性があります。

ところで、質問にはC ++というタグが付けられているため、参照カウントを維持して所有権を管理し、ポインターがスコープ外になった後にメモリを安全に削除する共有ポインターの使用を検討してください。ただし、循環依存関係のまれな使用ではデッドロックが発生する可能性があるため、注意して使用してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.