製品のご案内
  Visual Studio.NET
  技術ドキュメント
  サポート情報
  よくある質問
  ダウンロード
  登録とオーナーエリア
  関連情報
 


banner
Member of Visual Studio 技術ドキュメント

メモリ リークの検出と特定

Edward Wright
Microsoft

 

概要

メモリ リークが C/C++ プログラムの動作不良の原因になることはよくあります。Visual C++ のデバッガと CRT ライブラリを使って、メモリ リークの原因を突き止めることができます。以下のトピックで、その方法を説明します。

 

キーワード

メモリ リーク、動的割り当て、デバッグ、CRT

 

はじめに

メモリを動的に割り当てたり、メモリの割り当てを動的に解除したりする機能は、C/C++ プログラミング言語の最も強力な機能の 1 つですが、かつて中国の哲学者 "孫子" がいったように、最大の長所は最大の弱点にもなり得ます。C/C++ アプリケーションの場合がまさにそのとおりで、バグの原因の中でも最も多いのは、メモリの処理の誤りです。以前割り当てたメモリを適切に解放できないために生じるメモリ リークは、最もとらえがたく、検出しにくいバグの 1 つです。1 回しか発生しない小規模なメモリ リークは目立たないかもしれませんが、大量のメモリをリークするプログラムやリークの度合いが加速するプログラムでは、低パフォーマンスやパフォーマンスの段階的な劣化に始まり、メモリ不足による完全な動作異常に至るまでさまざまな症状が現れます。最悪の場合は、リークを引き起こしているプログラムが消費するメモリがあまりにも大量であるため、ほかのプログラムの動作まで異常になり、ユーザーはどこに本当の原因があるのかわからなくなります。また、無害なメモリ リークであっても、ほかの障害の前兆であることもあります。

幸い、Visual C++ のデバッガおよび CRT ライブラリには、メモリ リークを検出し、特定するための効率的なツールが付属しています。このホワイト ペーパーでは、これらのツールを使って、効率的に、系統的にメモリ リークを特定する方法を説明します。

このページのトップへ

 

メモリ リークの検出を使用可能にする

メモリ リークの検出に使われる主なツールは、デバッガと CRT デバッグ ヒープ関数です。デバッグ ヒープ関数を使用可能にするには、プログラムに以下のステートメントをインクルードする必要があります。

#define _CRTDBG_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

#include ステートメントは、ここに示した順番で挿入する必要があります。順番を変えると、使用する関数が正しく機能しないことがあります。crtdbg.h をインクルードすることにより、malloc 関数と free 関数が、それぞれのデバッグ バージョンである _malloc_dbg_free_dbg にマップされ、これらのデバッグ バージョンがメモリの割り当てと割り当ての解除を追跡します。このマッピングは、デバッグ ビルドでのみ (つまり _DEBUG が定義されているときにのみ) 行われます。リリース ビルドは、通常の malloc 関数と free 関数を使用します。

#define ステートメントは、CRT ヒープ関数のベース バージョンを、それに対応するデバッグ バージョンにマップします。このステートメントは絶対に必要というわけではありませんが、このステートメントがないと、メモリ リークのダンプに含まれる情報の有用性が半減します。

上記のステートメントを追加したら、プログラムに以下のステートメントを追加することによって、メモリ リーク情報をダンプすることができます。

_CrtDumpMemoryLeaks();

デバッガでプログラムを実行すると、_CrtDumpMemoryLeaks により、[アウトプット] ウィンドウの [デバッグ] タブにメモリ リーク情報が表示されます。メモリ リーク情報は、以下のように表示されます。

Detected memory leaks!

Dumping objects ->

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.

Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

#define _CRTDBG_MAP_ALLOC ステートメントを挿入しなかった場合のメモリ リークのダンプは、以下のようになります。

Detected memory leaks!

Dumping objects ->

{18} normal block at 0x00780E80, 64 bytes long.

Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

両者を比べてみればわかるように、_CrtDumpMemoryLeaks は、_CRTDBG_MAP_ALLOC が定義されているときの方が、はるかに有用な情報を提供してくれます。_CRTDBG_MAP_ALLOC が定義されていない場合は、以下の情報が表示されます。

  • メモリ割り当て番号 (中括弧に囲まれた数字)
  • ブロックの型 (Normal、Client または CRT)
  • 16 進数で表されるメモリの位置
  • バイト単位で表されるブロックのサイズ
  • 先頭の 16 バイトの内容 (16 進数)

_CRTDBG_MAP_ALLOC が定義されていると、リークしたメモリが割り当てられていたファイルも表示されます。ファイル名に続く括弧の中の数字 (下の例では 20) は、ファイル内の行番号です。行番号とファイル名が含まれている出力の行をダブルクリックすると、

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.

メモリが割り当てられているソース ファイル内の行 (上の例では leaktest.cpp の行番号 20) にカーソルが移動します。出力の行を選択し、F4 キーを押しても同じように動作します。

このページのトップへ

 

_CrtSetDbgFlag の使用

プログラムが常に同じ場所から抜け出すのであれば、_CrtDumpMemoryLeaks を呼び出すことは簡単です。しかし、プログラムが抜け出す場所がさまざまに変わる場合にはどうしたらよいでしょうか。この場合は、抜け出す可能性のあるすべての出口に _CrtDumpMemoryLeaks 呼び出しを挿入する代わりに、プログラムの先頭に以下のステートメントを挿入することで対処できます。

_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

このステートメントは、プログラムが抜け出すと自動的に _CrtDumpMemoryLeaks を呼び出します。上の例のように、必ず _CRTDBG_ALLOC_MEM_DF_CRTDBG_LEAK_CHECK_DF の両方のフィールドを設定する必要があります。

このページのトップへ

 

メモリ ブロックの型の解釈

既に説明したように、メモリ リーク情報では、リークしたメモリの個々のブロックが Normal ブロック、Client ブロックまたは CRT ブロックとして識別されます。実際には、Normal ブロックと Client ブロックしか表示されないことがほとんどです。

  • Normal ブロックは、プログラムによって割り当てられる通常のメモリです。
  • Client ブロックは、デストラクタを必要とするオブジェクトに対して MFC プログラムが使用する特殊なメモリ ブロックの型です。MFC new 演算子は、作成されるオブジェクトに応じて、Normal ブロックまたは Client ブロックのいずれかを作成します。
  • CRT ブロックは、CRT ライブラリが自分で使用するために割り当てるメモリのブロックです。CRT ライブラリは、これらのブロックの割り当ての解除を自分自身で処理するため、何らかの重大な障害 (たとえば、CRT ライブラリの破壊) が起きているのでない限り、この種のメモリがメモリ リーク レポートに現れることはほとんどありません。

以下の 2 つのブロックの型は、メモリ リーク情報にはまったく表示されません。

  • 解放されたメモリのブロックである Free ブロック。
  • メモリ リーク レポートに表示されないようにマークを付けたブロックである Ignore ブロック。

このページのトップへ

 

CRT レポート モードの設定

既に説明したように、デフォルトでは、_CrtDumpMemoryLeaks は、メモリ リーク情報を[アウトプット] ウィンドウの [デバッグ] ペインにダンプします。_CrtSetReportMode を使ってこの設定をリセットし、ほかの場所に情報をダンプすることができます。ライブラリを使用すると、ライブラリが出力をほかの場所にリセットすることがあります。その場合は、以下のステートメントを使って、出力の場所を [アウトプット] ウィンドウに戻すことができます。

_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );

_CrtSetReportMode を使って出力をほかの場所に送る方法についての詳細は、Visual C++ ドキュメントの『ランタイムライブラリ リファレンス』の _CrtSetReportMode!href(http://msdn.microsoft.com/library/devprods/vs6/vc++/vccore/_crt__crtsetreportmode.htm) を参照してください。

このページのトップへ

 

メモリ割り当て番号にブレークポイントを設定する

メモリ リーク レポートのファイル名と行番号は、リークしたメモリが割り当てられている場所を示しますが、メモリが割り当てられている場所がわかっても、必ずしも問題の原因が判明するわけではありません。多くの場合、プログラムの実行中は、割り当てが何度も呼び出されますが、メモリのリークは特定の呼び出しでしか起きないかもしれません。問題の原因を突き止めるには、リークしたメモリが割り当てられている場所だけでなく、リークが発生する条件も解明する必要があります。それを可能にする情報がメモリ割り当て番号です。この番号は、ファイル名と行番号の表示に続けて中括弧の中に表示されます。たとえば、以下の出力では、「18」がメモリ割り当て番号です。これは、リークしたメモリがプログラム内で18番目に割り当てられたメモリのブロックであることを示しています。

Detected memory leaks!

Dumping objects ->

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.

Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

CRT ライブラリは、CRT ライブラリ自身や MFC など、ほかのライブラリによって割り当てられるメモリを含む、プログラムの実行中に割り当てられるすべてのメモリ ブロックの数をカウントします。したがって、割り当て番号 N のオブジェクトは、プログラム内で N 番目に割り当てられたオブジェクトですが、コードによって N 番目に割り当てられたオブジェクトではないかもしれません (ほとんどの場合、両者は一致しません)。

割り当て番号を使って、メモリが割り当てられている場所にブレークポイントを設定することができます。これを行うには、まず、プログラムの先頭付近に位置ブレークポイントを設定します。プログラムがその位置で中断したときに、[クイック ウォッチ] ダイアログ ボックスまたは [ウォッチ] ウィンドウで、メモリ割り当てブレークポイントを設定することができます。たとえば、[ウォッチ] ウィンドウで、[シンボル名] カラムに以下の式を入力します。

_crtBreakAlloc

CRT ライブラリのマルチスレッド DLL バージョン (/MD オプション) を使っている場合は、以下のようにコンテキスト演算子を追加する必要があります。

{,,msvcrtd.dll}_crtBreakAlloc

ここで Enter キーを押します。デバッガが呼び出しを評価し、結果が [値] カラムに表示されます。メモリ割り当て位置にブレークポイントが設定されていない場合、この値は ?1 になります。[値] カラムの値を、実行を中断したいメモリ割り当て位置の割り当て番号に変更します。たとえば、上記の出力が示す割り当て位置で実行を中断するには、値を 18 に設定します。

注目しているメモリ割り当て位置にブレークポイントを設定したら、デバッグを継続することができます。割り当ての順番が変わらないように、前回実行したときと同じ条件でプログラムを実行する必要があるので注意してください。指定されたメモリ割り当て位置でプログラムの実行が中断されたら、[コール スタック] ウィンドウやその他のデバッガ情報を参照し、メモリが割り当てられたときの条件を調べます。必要な場合は、その位置からプログラムの実行を再開し、オブジェクトに何が起きるかを確認すれば、割り当てが適切に解除されない理由が判明することもあります。(注意: オブジェクトにデータ ブレークポイントを設定する方法も効果的です。)

通常は、デバッガでメモリ割り当てブレークポイントを設定する方が簡単ですが、コード内でブレークポイントを設定することもできます。コード内でメモリ割り当てブレークポイントを設定するには、以下のような行を追加します (18 番目のメモリ割り当ての場合)。

_crtBreakAlloc = 18;

代わりに、同じ機能を持つ _CrtSetBreakAlloc 関数を使うこともできます。

_CrtSetBreakAlloc(18);

このページのトップへ

 

メモリ状態の比較

メモリ リークの発生源を突き止める別の方法は、重要な位置でアプリケーションのメモリ状態のスナップショットを取り込む方法です。CRT ライブラリに付属している構造体型 _CrtMemState を使って、メモリ状態のスナップショットを格納することができます。

_CrtMemState s1, s2, s3;

特定の位置でメモリ状態のスナップショットを取り込むには、_CrtMemState 構造体を _CrtMemCheckpoint 関数に渡します。この関数は、構造体に現在のメモリ状態のスナップショットを格納します。

_CrtMemCheckpoint( &s1 );

_CrtMemState 構造体を _CrtMemDumpStatistics 関数に渡すことにより、任意の位置の _CrtMemState 構造体の内容をダンプすることができます。

_CrtMemDumpStatistics( &s3 );

この関数は、以下のようなメモリ割り当て情報のダンプを出力します。

0 bytes in 0 Free Blocks.

0 bytes in 0 Normal Blocks.

3071 bytes in 16 CRT Blocks.

0 bytes in 0 Ignore Blocks.

0 bytes in 0 Client Blocks.

Largest number used: 3071 bytes.

Total allocations: 3764 bytes.

メモリ リークがコードの特定のセクションで発生したかどうかを判断するには、そのセクションの前後のメモリ状態のスナップショットを取り込み、_CrtMemDifference を使用して 2 つの状態を比較します。

_CrtMemCheckpoint( &s1 );

// memory allocations take place here

_CrtMemCheckpoint( &s2 );

if ( _CrtMemDifference( &s3, &s1, &s2) )

_CrtMemDumpStatistics( &s3 );

関数の名前が示しているように、_CrtMemDifference は 2 つのメモリ状態 (2番目のパラメータと3番目のパラメータ) を比較し、2 つの状態の差である結果 (1 番目のパラメータ) を生成します。_CrtMemCheckpoint 呼び出しをプログラムの先頭と末尾に挿入し、_CrtMemDifference を使って結果を比べることによってもメモリ リークの有無をチェックすることができます。リークが検出された場合は、_CrtMemCheckpoint 呼び出しを使って、プログラムを分割し、バイナリ サーチ法を使ってリークの場所を突き止めることができます。

このページのトップへ

 

補足情報

以下の記事では、特に MFC アプリケーションを中心として、メモリ リークのデバッグについて論じています。

  • 1997 年 11 月 Pinnacle Publishing 発行、Mike Blasczak 著、『Visual C++ Developer』の「Stunt Debugging: Diagnosing Memory Leaks」

以下の書籍はメモリ リークのデバッグにはまったく関係ありませんが、ホワイトペーパーを書いていて、昔の中国の哲学者の言葉を引用したいと思っている読者には役に立つかもしれません。

  • 孫子著『兵法』、取扱出版社多数。

このページのトップへ

(C) 1999 Microsoft Corporation. All rights reserved. Terms of Use.