C/C++、Fortran 90 アプリケーションのための高度なメモリーデバッガーおよびメモリーリーク検出ツール

メモリーデバッグ

デバッガーの機能および特長

Allinea DDT のメモリーデバッガーは、Linux* 上でのC/C++、および Fortran コードにありがちな多くのメモリーエラーの修正を支援します。このモードの機能は、コマンドライン・デバッガーや print 文によるデバッグよりもはるかに優れています。

メモリーデバッグは、DDT のチェックボックスをオンにするか、コマンドラインに "--mem-debug" を追加することで有効になります (Cray XC もしくは XK プラットフォームに関しては、リンクするためのさらなるステップが必要となります)。

Allinea DDT を使うことにより、次のような課題の答えを見つけ、問題を解決することができます:

  • どの程度メモリーを消費しているか
  • 最もメモリーが割り当てられているコードはどこか
  • メモリーリークは存在するか、どこでメモリーの解放を失敗しているか
  • 解放後のポインターが利用され、アプリケーションのクラッシュの原因になっているか
  • ポインターはまだ有効なのか、どこにどの程度のメモリーが割り当てられているのか
  • アプリケーションが無効なポインターを正しく解放しているか
  • 範囲外の領域のメモリーを読み書きしていないか

これらの課題の答えを見つけることで、予期せぬクラッシュを防ぐことができます。ここに挙げられた課題を解決することは、ソフトウェアの品質改善にもつながります。

ヒープ

メモリー・デバッグ・モードによって検知されるメモリーの領域をヒープといいます。ヒープとは、C ではmalloc、free、その他類似の関数、C++ では new/delete オペレーター、Fortran 90 あるいはそれ以降の Fortran 派生言語においては allocate/deallocate などのプリミティブ型によって管理されている領域を指します。

DDT は、これらの関数をインターセプトしてエラーを検出し、情報を記録し、またメモリーがどれだけ使用されているかを測定します。

チェックのレベルは、メモリーデバッグの設定ダイアログにある設定レベルでベーシックモードからフルモードに調節できます。フルモードでは、割り当て数の多いコードのスピードを下げる可能性がありますが、ベーシックモードでは時間的なコストはほとんどありません。

メモリー使用量

メモリーの総使用量に注目することは重要です。メモリーを過度に割り当てた結果、プロセスがオペレーティング・システムに強制終了されてしまうことも考えられます。

プロセスのメモリー使用量は、[Tools (ツール)] > [Overall Memory Stats (全体のメモリー統計)] メニューで表示できます。 複数のプロセスをデバッグする場合には、それぞれのプロセス (または使用度の高いプロセス群) は、次のように表示されます。

この表示画面は、メモリーデバッグ機能が有効化されている場合に利用できます。総使用量が上昇した場合には、メモリーリークが示唆されます。

DDT を含むツールスイート、Allinea Forge にも、パフォーマンス・プロファイラーである MAP の機能としてメモリー・プロファイリングが存在します。MAP では、メモリーの使用状況が変化する様子がグラフィック表示されますので、いつどこでメモリーの使用量が増加したのかを絞り込むことができます。

メモリーリークの検出

メモリーが割り当てられた後に解放されなければ、結果的にメモリー不足の問題が生じ、突然プログラムが終了してしまいます。

メモリーリークは、メモリーデバッグが有効化されている場合、[Tools (ツール)] > [Current Memory Usage (現在のメモリー使用量)] メニューで検出できます。

割り当てられたメモリーに対し、DDT は個別にスタックトレースおよび指定した割り当てサイズを記録します。これにより、コードのどこで割り当てがなされ、どれだけのメモリーを使用しているかが確認できます。

[Current Memory Usage] ダイアログはこの情報を使い、使用バイト数を単位とした呼び出しの先頭の位置をグラフに描画します。リークが起こった場合には、この棒グラフにはっきりと表示されます。棒グラフの要素をクリックするとポインターがリストアップされ、さらにその中からポインターを選択すると、その割り当てのスタックとサイズが表示されます。

カスタム・アロケーターとクラス・コンストラクター

多くのコードは少数のエントリーポイントを介して割り当てを行っています。

たとえば、メモリーの割り当てには C++ クラスのコンストラクターがよく使用されます。プログラム中を通して、最も有用なクラスが呼び出されます。この場合、コンストラクターを呼び出したコード行によって割り当てをグループ化すると、コンストラクターの呼び出しが不定形な塊としてひとまとめにされないためより有益です。

コンストラクターの呼び出しごとにグループ化するには、棒グラフのブロックを右クリックし、その関数を “Custom Allocator (カスタム・アロケーター)” として追加します。その関数の呼び出しは、呼び出し位置によって個別にグループ化されます。

リーク検出の自動化と回帰テスト

DDT には、上記で説明した [Current Memory Usage] ツールの対話型情報に類似した、非インタラクティブ型のメモリー・デバッグ・モードがあります。このモードは、メモリーの使用量を測定し、リークが実動コードに入り込まないことを自動的に保証するため、夜間テスト中や CI サーバーなどでよく使われます。

このモードでは、プロセスの終了後にも残されたままとなっているメモリーの割り当て情報が記載された HTML ファイルが作成されます。

ddt --offline offline-log.html --mem-debug ... application.exe ....

これにより、プログラムの実行中にログされた重大なデバッグイベントやリークレポートを含む、非インタラクティブ・モードにおけるデバッグセッションの注釈付きログファイルが作成されます。

無効なポインターの解放

すでに解放されている割り当てや、割り当てられていない、または正規のヒープ領域に到達していない偽のアドレスを解放しようとする時にもクラッシュが起こる場合があります。

結果として即時終了、ヒープ・コラプション、あるいは将来的なクラッシュにつながります。

DDT では、無効なポインターが検出されると即エラーメッセージがトリガーされ、エラーの起こった正確な場所でプロセスを停止するので、このような問題を回避することができます。

ダングリング・ポインター

ダングリング・ポインターとは、解放されたものの null に設定されていないメモリーのポインターのことを指します。あとに続くコードがダングリング・ポインターを使い続けると、データはまるで有効に見えますが、ある時突如としてメモリーが別の場所に再割り当てされてしまう可能性があります。

ダングリング・ポインターは、予期できない動作、サイレント・データ・コラプション、またはプログラムのクラッシュを引き起こす原因となります。

ダングリング・ポインターの検出機能を有効にするには、メモリーデバッグが Fast よりも1つ上のレベルに設定されている必要があります。[Enabled Checks] ウィンドウに “free-protect” という言葉が表示されます。

ダングリング・ポインターの問題を検出するには、通常はこのレベルのメモリーデバッグで十分だと考えられます。ダングリング・ポインターが再使用されると、再利用したコードで正確にプログラムが停止し、次のようなエラーメッセージが表示されます:

デバッガーでは、どのポインターがダングリングしていて、もともとそれがどこで割り当てられたのかも表示されます。いずれかのポインターまたは動的に割り当てられた配列を右クリックし、メニューから [View Pointer Details (ポインターの詳細情報を表示)] を選択します:

Allinea DDT では、どの特定のポインターがダングリングしているかが直ちにわかり (ポインターがすでに解放された割り当てを指していることを示します)、その割り当てに起因する関数呼び出しのフルスタックを表示します。

ポインター情報の確認

上のダングリング・ポインターのセクションで説明したとおり、[Pointer Details] ウィンドウにはポインターに関する詳細情報が表示されます:

  • ポインターの有効性、割り当てやダングリングの有無
  • 割り当てサイズ
  • のちに解放されたダングリング・ポインターを含む、ポインターの割り当てられたコード内の正確な場所およびスタック
  • 実際にダングリング・ポインターである場合、ポインターが再度解放されたコード内の正確な場所およびスタック

特に上の画像では、ポインターが hello.c:88 で割り当てられたということが確認でき、そこをクリックすることで直ちにソース・コード・ビューアー内でその場所に移動できます。

このメモリーがどこで解放されたのかを知ることもできます。

配列範囲外またはメモリーの割り当て範囲外への読み取り/書き込み

配列範囲外またはその他のメモリーの割り当て範囲外で値を読み取ることは、好ましいことではありません。

大半の場合には見過ごされ、問題が発生しないこともありますが、ランタイム環境でのごく僅かな変更がクラッシュの有無を左右し、断続的なクラッシュを引き起こす可能性もあります。

  • 割り当て範囲外の値を読み取ることで、戻り値またはコードパスが信頼性の低く不確かな要素に依存してしまうため、計算に誤差が生じる原因となります。
  • 割り当て範囲外での書き込みは、書き込みを行ったことにより破損してしまったこの場所を再利用する別のコードで不確かな動作を引き起こす可能性があります。
  • アドレスがプログラムの割り当てページ以外の場所にある場合、読み取りも書き込みもクラッシュの原因になり得ます(通常は 4096 バイトですが、システムによってはさらに大きなページを使用しています)。

DDT では、このようなエラーを回避できます。オペレーティング・システムに働きかけて各割り当ての前後にページを作成し、それぞれを読み取り/書き込み禁止にします。保護されているメモリーに読み取りまたは書き込み操作が行われようとすると、Linux* オペレーティング・システムがデバッガーに通知します。これらのページは “ガードページ” または “レッドゾーン” として知られています。

大半の科学計算を行うコードやFortran 90 コードなど、大きな割り当ての比較的少ないコードに関しては、ガードページとして使われるページ数も少なくなります。

C++ コードは、概して小さい割り当てを大量に使用しますので、プロセスリミットに到達してしまう可能性があります。そのようなコードに関しては、DDT では、“フェンス・チェッキング” または “フェンス・ペインティング” として知られる 代替設定を提供しています。この設定では、割り当てメモリーの上下数バイトを定期的に検証し、予期しない書き込みがされていないかを確認します。このモードがチェックするのは書き込みだけで、誤った読み取りは検出できないため、可能な限りガード・ページ・モードの方が優先的に使用されています。

その他情報

  • メモリーデバッグに関するトピックは、ブログを参照してください。

製品カタログ ダウンロード (PDF、英語)