web-dev-qa-db-ja.com

ゼロのルールに従う場合、オブジェクトの構築をどのようにデバッグしますか?

コードレビュー、ピアカンバセーション、スタックオーバーフローのコメント/回答で誰もが「ゼロのルール」をすぐに指摘します。

私は信者ではありません。私はなりたいです。通常、十分な数の人々がそれに同意して独自のラベルを取得し、イディオムになる場合、それはよく考えられています。

だから、私が信じない理由を説明しましょう:

本番環境のクラッシュからクラッシュダンプをデバッグするように命じられた回数を数えることができません。上記のダンプをデバッグしているときに、コールスタックを調べたり、調べたりすることができます。「このクラスはいつ構築されるのですか」を確認する必要がある場合があります。第二に、「それが何であるか、またはそのメンバーのアドレスは何か」を確認する必要があるかもしれません。これは通常、必要以上のインスタンスが作成されているという証拠の後、またはアドレスやサイズが予期したものではない場合に、コード内の他の場所につながります。

これは、「このクラスがいつ割り当てられるのか」を確認したい場合にも当てはまります。

したがって、ソースを変更せずにダンプ内の状態を再現しながら、将来のデバッグセッションを中断する場所を提供するために、私は常に5つのメソッドすべてを実装しています。これらがなければ、上記のデバッグセッションを(簡単に)実行する方法はありません。

ブレークするコード行を提供するためにソースを変更すると、クラッシュダンプはソースで使用できなくなります。

さて、簡単なコンストラクタと代入演算子の場合、この状況は、コードを追加したり、潜在的なバグを追加したりするほど頻繁には起こらないと言う人もいます。ただし、問題のデバッグを開始するために、別のビルドを作成し、本番環境に再デプロイし、クラッシュを再現し、別のダンプを取得する必要があることを上司に説明する必要がある場合でも、問題をデバッグするには十分だと私は主張します見通し。それは楽しい経験ではなく、何度も私に起こりました。

だから、私を信じてください!

いい子になって「Rule of Zero」を実行した後、ブレークダンプを利用できる行がないときに、クラッシュダンプが与えられたときに上記のデバッグセッションをどのように実行できるかを教えてください。

つまり:

  1. ブレークするコンストラクターコードの行なしで、特定のクラスのインスタンスが構築されているときはいつでもブレークします。

  2. 特定のクラスのインスタンスが割り当てられているときはいつでもブレークします。

私はVisual Studio、より具体的にはVisual Studio 2015を使用しています。

7

多くの議論、多くの昼食、さまざまなフォーラムへの投稿、そして頭を悩ませた後、私の同僚は次のことを思いつきました:

Visual Studioで、[デバッグ]-> [新しいブレークポイント]-> [関数ブレークポイント]に移動し、ブレークしたいコンパイラー生成メソッドの完全修飾名を入力します。たとえば、Foo :: Fooは、Fooのコンパイラーが生成したデフォルトのコンストラクターで中断します。コピーコンストラクターにはFoo :: Foo(const Foo&)、移動コンストラクターにはFoo :: Foo(Foo &&)などを追加できます。名前空間も含める必要があることに注意してください。また、存在しない関数名を追加できることにも注意してください。ブレークポイントウィンドウの不透明な赤い点と、ヒットしない点を示す空の円に注意してください。

デバッガーが中断した後、コールスタックウィンドウを使用して、デバッガーが壊れた部分を特定し、続行するかどうかを選択できます。検査したい場合は、逆アセンブリウィンドウを開き、ステップ(F10)を実行して、コンストラクター、コピーコンストラクター、割り当てなどが完了します。私たちを導くための「ret」命令を探すことができます。逆アセンブルウィンドウでも、コールスタックを1つ上に移動すると、メソッドの名前が示されます。次に、ローカルに切り替えて、「これ」を通常どおり表示できます。

もちろん、これはVisual Studioに固有のものです。他のデバッガーも同様のものを持っているかもしれません。

12

クラッシュダンプは、元のソースコードから生成された元のバイナリとデバッグシンボル、ソースコードの変更または再コンパイルを求める提案でのみトラブルシューティングできるため、現在の状況では役に立ちません( OPがすでに受け取ったクラッシュダンプの数)。これらの提案は、ソフトウェアの将来のリリースのクラッシュダンプのトラブルシューティングの予後を改善するだけです。

(Rant。)ある種のデータの破壊とプログラムと実行状態の再構築を含む明白でないクラッシュダンプを調査するためのテクニカルサポートの一般的なコストは約1000ドルであることを言及しなければなりませんか?将来1つのクラッシュダンプを防ぐために1週間の作業を費やすことができるなら、それは価値があるようです。ここでは(低コストの国への)アウトソーシングの機会が見られますが、コアダンプをサードパーティに調査させるには、会社の具体的なソフトウェアの秘密をすべて公開することを意味します。


一般に、Visual Studioのステップデバッガーに表示されるアセンブリコードを読み取って理解する機能は、クラッシュダンプを操作するための要件です。

選択肢は次のとおりです。

  • このタスクを自分で処理する能力を身につけてください。
    • クラッシュダンプが与えられたときに、クラッシュ前のプログラムの状態と実行のパスの理解を再構築するためのアセンブリコードとテクニックの読み方を学びます。
  • タスクを再割り当てできるように上司に知らせてください
  • このタスクを他の人に委任する

同じ状況に直面したときに使用するテクニック。

クラッシュダンプを処理する最初のルールbefore同じクラッシュを自分で再現してみてくださいです。クラッシュにつながる条件を再現できる場合は、クラッシュしたプログラム(つまり、それをステップ実行できます)のトラブルシューティングライブインスタンスではなく、凍結したインスタンス(つまり、ステップ実行を許可しないクラッシュダンプ)。

関数がインライン化されていることがわかっている場合は、すべての呼び出し元(技術的には "call sites" )を見つけて、そこにブレークポイントを設定しようとします。呼び出し元もインライン化されている場合は、インライン化されていない呼び出しサイトが見つかるまで、親の呼び出し元を追跡します。

可能性のあるすべての呼び出しサイトを見つけるために、わずかに変更されたソースコードの再構築でそれを行うことができることに注意してください。 「考えられるすべての呼び出しサイト」の知識は、ソースコードの1つのバージョンから取り除くことができる知識であり、その知識のほとんどは、作業中のクラッシュダンプにも適用できます。

構造体を追跡するには、そのアドレスを知る必要があります。構造体のアドレスが逆アセンブリコードで明らかにされる典型的な方法は、そのアドレスが関数呼び出しに渡されたときです。


「ゼロの法則」に問題はありますか?

私はそれがしばしば誤って適用されることにも同意し(そしてこの問題が将来のリリースのソースコードで修正されればもっと良いでしょう)、そして私自身の経験を共有します。


個人的には、コンストラクタ/デストラクタの少なくとも1つが 簡単なデフォルト でない場合、ほとんどのC++クラスの「3のルール」または「5のルール」に従います。

これを行う理由は2つあります。

最初の理由はあなたと同じです。コンストラクタが自明なデフォルトではない場合、それは、コンパイラが生成する必要がある「コード」があることを意味します。これらの生成されたコードは、クラスの一部のメンバーのコンストラクターなど、他のコードを呼び出す必要がある場合があります。これらの他のコードが自分で作成した場合は、それらをデバッグできるようにする必要があります。ユーザー定義コンストラクターを提供すると、このタスクが容易になります。

2番目の理由は、コンストラクターが暗黙的に宣言されている場合、状況によっては、その型をSTLベクトルに格納しようとすると問題が発生する可能性があることです。表面的な理由は不完全型の問題に関連していましたが、根本的な理由は不明であり、この質問には関連しないため、ここで終了します。


ただし、ユーザー定義のコンストラクターを提供しても、これらのメソッドのinliningが常にオフになるわけではありません。コンパイラーはそれらの一部をインライン化する場合があります。とにかくコンストラクタ。インライン化を本当に防ぐには、MSVCでdeclspec(noinline)を使用します。

最適化されたビルド(「リリース」構成で作成されたビルド)で逆アセンブリデバッグを使用する機能は、インライン化とMSVCのバージョンの両方に依存します。 Visual C++ 2017では、インライン展開された関数の逆アセンブリアドレスとコード行の場所の間のマップを維持するための処理が向上しています。


逆アセンブリデバッガーは、本当に空であるため、時々、空のコンストラクターをスキップしました。 C互換の「POD」タイプは、通常、memset(p, 0, sizeof(*p))またはXOR RAX, RAX; MOV [...], RAXのシーケンスで初期化され、効果的にゼロを埋める命令であると考えられています。場合によっては、それらがまったくゼロで埋められていない、つまり、指示がない場合があります。逆アセンブリデバッガーは、存在しない逆アセンブリアドレスで停止することはできません。


控えめな発言のように、私はそれを見つけます:

  • タイプのデストラクタは、より多くの「逆アセンブリで表示されるもの」を生成するように見えます。最も顕著なのは、デストラクタが動的メモリ(delete)の削除の一部として呼び出された場合です。逆アセンブリでは、some_type::scalar deleting destructorに関連付けられたアドレスへの実際の関数呼び出しが表示されます。モジュール(EXEまたはDLL)がプロセス空間に読み込まれると、逆アセンブリアドレスにブレークポイントを設定できます。

  • この質問には関係のない理由により、プロジェクトでは2フェーズの初期化を行う傾向があります。しかし、このプロジェクトガイドラインにより、エラーはコンストラクターからではなく、重い作業を行う2番目の関数からスローされる傾向があることがわかりました。

  • 文字列メッセージでも例外を使用します。プロジェクトで例外を使用しない場合は、エラーの詳細をキャプチャするための代替手段としてエラーログを検討できます。


構造体で問題をデバッグする鍵は、そのアドレスを確実に知る(記録する)ことです。多くのテクニックがあります。ただし、これらの手法についての議論は、議論の範囲を広げすぎます。

2
rwong

多くの抽象化に依存することの避けられない結果は、特定のことを行う能力の喪失です。呼び出される仮想関数にブレークポイントを配置できますが、仮想ディスパッチロジックitselfにブレークポイントを配置することはできません。つまり、呼び出された関数は派生クラスのオーバーライドのいずれかである可能性があるため、基本クラスの仮想関数をだれがいつ呼び出したかを知ることはできません。

特別なメンバー関数についても同じことが言えます。コンパイラがコードを生成しましたが、ブレークポイントを配置する場所がありません。それが人生だ。

したがって、ゼロのルールの利点(DRYによる保守の容易さ、実装の容易さ(自己割り当てを確認するのを忘れたことはありますか?)など)を選択するか、関数を手動で作成するデバッグ機能を利用できます。または、両方を実行することもできます。特別なメンバー関数にブレークポイントを設定する場合は、特別なメンバー関数then。を記述し、デバッグが完了したら削除します。

コンパイラが生成した関数を使用することの利点には、他の方法ではできないが得られることが含まれることにも注意してください。たとえば、ささいなコピー可能性。 write簡単なコピーコンストラクタは使用できません。コピーコンストラクターの実装を提供する場合、たとえコンパイラーのコンパイラーが行うとおりに実行しても、型はTriviallyCopyableではなくなります。

そして、それはあなたが軽くあきらめるべきものではありません。

1
Nicol Bolas

私の意見では、「ゼロのルール」はルールではありません。 デザインパターンです。あなたはそれを使用することでポイントを獲得することはなく、あなたはそれに従わなかったとしてゼロポリスのルールによって罰せられません*;しかしmightコードを読みやすく、保守しやすくします。


私の意見では、「ゼロの規則」は、ロギング機能を実行するコンストラクター、またはそこにブレークポイントを設定できるように存在するコンストラクターを作成することを禁じていません。コンストラクターは、オブジェクトのメンバーの明示的な初期化を実行する場合にのみルールに違反します。


または、多分そうです。その場合は、別の雇用者を探すことを検討してください。

0
Solomon Slow