web-dev-qa-db-ja.com

メモリ破損のデバッグ

まず、これは完全な回答を持つ完全なQ&Aスタイルの質問ではないことに気づきますが、それをより効果的にするための言い回しは考えられません。これに対する絶対的な解決策はないと思います。これがスタックオーバーフローの代わりにここに投稿する理由の1つです。

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

いくつかの負荷をシミュレートするためにプリミティブボットを実装した場合でも、それを確実に再現できないようです。クラッシュが発生しません(何かを引き起こしたlibeventの問題を修正しました)

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

それから地獄をバリグリンディング-物事がクラッシュするまで無効な書き込みはありません(本番環境では1日以上かかる場合もあれば、わずか1時間かかるかもしれません)。これは本当に困惑します。確かにある時点で、無効なメモリにアクセスして上書きすることはありません。機会? (アドレス範囲を「広げる」方法はありますか?)

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

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

コードレビュー!自分自身で(徹底的に)、何人かの友人に私のコードを見てもらっていますが、それで十分だとは思えません。開発者を雇ってコードレビューやデバッグを行うことを考えていたのですが、多額の資金を投入する余裕がなかったため、少しのために働く意思のある人をどこで探すべきかわかりませんでした。問題が見つからなかったり、資格のある人がいない場合は、お金はありません。

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

全体として、この問題は2か月近く(オンとオフ、さらに趣味のプロジェクトのように)忙しくしており、私が不機嫌なIRLになり、あきらめることを考えているところまで本当にイライラしています。私は問題を見つけるために他に何をすべきかについて考えることができません。

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

編集:

重要な場合のいくつかの仕様:

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

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

David.pfxの投稿に応じて編集:(応答が遅いため申し訳ありません)

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

はい、私はまだ最近のクラッシュのダンプを抱えています

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

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

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

まだパターンは見つかりませんでしたが、少しランダムに見えます。どこから汚職が始まったのかわからないのでわかりません。

ヒープ関連ですか?

それは完全にヒープ関連です(私は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;
_

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

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

多くのコールバック(非同期ネットワークI/O、タイマー)が使用されていますが、問題はありません。

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

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

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

詳しく説明しますか?これは、上記のキャッシュプロセスの場合に多少当てはまります。私が頭の上から想像できることは、それが十分に速く終了せず、ガベージデータを使用しないことですが、それもネットワークを使用しているため、そうではありません。同じパケットモデル。

23
Robin

それは挑戦的な問題ですが、私はあなたがすでに見たクラッシュに見つかる多くの手がかりがあると思います。

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

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

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

必死で、状態を保存して自動再起動できますか?それを行ういくつかの製品ソフトウェアを考えることができます。

お手伝いできることがあれば、遠慮なく詳細を追加してください。


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

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

これらは、焦点を合わせるコードの部分です。

21
david.pfx

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

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

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

6
ddyer

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

あなたの説明から、私の個人的な推測では、ポインタへの参照をどこかでメモリを解放するために保持していて、自分のものではなくなったポインタを解放したり、書き込みを行ったりする可能性があります。上記の手法で監視するサイズ範囲を推測できる場合は、ログを大幅に絞り込むことができます。それ以外の場合は、破損しているメモリを見つけたら、ログからそのメモリに至ったmalloc/freeパターンを簡単に特定できます。

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

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

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

3

質問で言うことを言い換えると、明確な答えを出すことはできません。私たちにできる最善のことは、探すべきことやツールやテクニックを提案することです。

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

症状から

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

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

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

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

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

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

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

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

3
andy256

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

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

0
Mohammad Azim