次の方法で共有


   バッファの上書きおよびメモリ リークの解決法

プログラミング中に発生する問題には、割り当てられたバッファの末尾が上書きされることと、メモリのリーク (割り当てが必要なくなった場合に、割り当ての解放に失敗すること) の 2 つがあります。このようなメモリ割り当ての不具合を解決するために、デバッグ ヒープには強力なツールが用意されています。

ヒープ関数のデバッグ バージョンにより、リリース ビルドで使用される標準バージョン、またはベース バージョンが呼び出されます。メモリ ブロックを要求すると、デバッグ ヒープ マネージャにより、ベース ヒープから、要求よりも多少大きいメモリー ブロックが割り当てられ、このブロックの中で使用できる部分へのポインタが返されます。たとえば、アプリケーションで malloc(10) が呼び出されるとします。リリース ビルドでは、malloc により、ベース ヒープ割り当てルーチンが呼び出され、10 バイトの割り当てが要求されます。しかし、デバッグ ビルドでは、"malloc" によって _malloc_dbg が呼び出され、次に、_malloc_dbg によってベース ヒープ割り当てルーチンが呼び出されて、10 バイトの割り当てに加え、約 36 バイトのメモリの追加割り当てが要求されます。デバッグ ヒープでの結果メモリ ブロックはすべて、割り当てられた順序に従って、1 つのリンクされたリストに接続されます。

デバッグ ヒープ ルーチンによって割り当てられた追加メモリは、管理作業情報、デバッグ メモリ ブロックをリンクするポインタ、データのどちらかの側にある、割り当てられた領域の上書きを検知する小さなバッファのために使用されます。

デバッグ ヒープの管理作業情報を格納するために使用されるブロック ヘッダー構造体は、Dbgint.h ヘッダー ファイルで、次のように宣言されます。

typedef struct _CrtMemBlockHeader
{
// この直前に割り当てられたブロックへのポインタ
   struct _CrtMemBlockHeader *pBlockHeaderNext;
// この直後に割り当てられるブロックへのポインタ
   struct _CrtMemBlockHeader *pBlockHeaderPrev;
   char *szFileName;   // ファイル名
   int nLine;          // 行番号
   size_t nDataSize;   // ユーザー ブロックのサイズ
   int nBlockUse;      // ブロックのタイプ
   long lRequest;      // 割り当て番号
// ユーザー メモリの直前にある (ユーザー メモリより低い位置にある) バッファ
   unsigned char gap[nNoMansLandSize];
} _CrtMemBlockHeader;

/* デバッグ ヒープにある実際のメモリ ブロックでは、
 * この構造体の後に次の 2 行が続きます。
 *    unsigned char data[nDataSize];
 *    unsigned char anotherGap[nNoMansLandSize];
 */

ブロックのユーザー データ領域のどちらか一方にある NoMansLand バッファのサイズは 4 バイトで、デバッグ ヒープ ルーチンにより、ユーザー メモリ ブロックの限界が上書きされていないことを確認するために使用される既知のバイト値でいっぱいになっています。また、新しいメモリ ブロックも既知の値でいっぱいです。以下に説明するように、解放されたブロックをヒープのリンクされたリストに保存する場合は、これらの解放されたブロックも既知の値でいっぱいになります。実際に使用されるバイト値は次のとおりです。

  • NoMansLand (0xFD)
    アプリケーションによって使用されるメモリのどちらか一方にある NoMansLand バッファは 0xFD でいっぱいになっています。
  • 解放されたブロック (0xDD)
    _CRTDBG_DELAY_FREE_MEM_DF フラグが設定されている場合に、デバッグ ヒープのリンクされたリストで未使用のままになっている解放されたブロックは 0xDD でいっぱいになっています。
  • 新規オブジェクト (0xCD)
    新しいオブジェクトは、割り当てられたときに、0xCD でいっぱいになります。

デバッグ ヒープにあるブロックのタイプ

デバッグ ヒープにあるメモリ ブロックは、それぞれ、5 種類の割り当てタイプのうち 1 つに割り当てられます。リーク検知と状態の報告のために、タイプごとに異なるトラックやレポートが行われます。_malloc_dbg などのデバッグ ヒープ割り当て関数の 1 つを直接呼び出して、割り当てることにより、ブロック タイプを指定することができます。デバッグ ヒープにある 5 種類のメモリ ブロック タイプは次のとおりです。これらは _CrtMemBlockHeader 構造体の nBlockUse メンバに設定されています。

  • _NORMAL_BLOCK
    malloc または calloc を呼び出すと、Normal ブロックが作成されます。Normal ブロックだけを使用し、Client ブロックが必要ない場合は、_CRTDBG_MAP_ALLOC を定義すると、ヒープ割り当て呼び出しがすべて、デバッグ ビルドにあるデバッグ ヒープ割り当て呼び出しにマップされるようになります。この結果、各割り当て呼び出しのファイル名や行番号情報が、対応するブロック ヘッダーに格納されます。

  • _CRT_BLOCK
    多数のランタイム ライブラリ関数によって内部的に割り当てられたメモリ ブロックは、Crt ブロックとしてマークされ、ほかのブロックとは別に処理することができます。結果として、リーク検知などの操作がこれらのブロックの影響を受けなくなります。アロケーションで、Crt タイプのブロックの割り当て、再割り当て、解放を行うことはできません。

  • _CLIENT_BLOCK
    アプリケーションはデバッグの目的で、与えられた複数のアロケーションをこのタイプに割り当て、デバッグ ヒープ関数を明示的に呼び出すことにより、これらのアロケーションを特別にトラックすることができます。たとえば、MFC により、CObjects はすべて Client ブロックとして割り当てられます。ほかのアプリケーションでは、異なるメモリ オブジェクトが Client ブロックに格納されます。より詳細なトラックを行うために、Client ブロックのサブタイプを指定することもできます。Client ブロックのサブタイプを指定するには、数値を左方向に 16 ビット シフトし、_CLIENT_BLOCK との OR をとります。例を次に挙げます。

    #define MYSUBTYPE 4
    _free_dbg(pbData, _CLIENT_BLOCK|(MYSUBTYPE<<16));
    

    Client ブロックに格納されたオブジェクトをダンプするためのクライアント提供のフック関数は _CrtSetDumpClient を使用してインストールされ、Client ブロックがデバッグ関数によりダンプされる場合に、必ず呼び出されます。また、_CrtDoForAllClientObjects を使用して、デバッグ ヒープにある Client ブロックそれぞれに対して、アプリケーションにより供給された関数を呼び出すことができます。

  • _FREE_BLOCK
    通常、解放されたブロックはリストから削除されます。解放されたメモリにまだなにも書き込まれていないことを確認したり、低メモリ状態をシミュレートしたりするには、解放されたブロックをリンクされたリストに残し、Free とマークして、既知のバイト値 (現在は 0xDD) でいっぱいにしておくことができます。

  • _IGNORE_BLOCK
    一時的に、デバッグ ヒープ操作をオフにすることができます。この間、メモリ ブロックはリストに残されたままになりますが、Ignore ブロックとマークされます。

デバッグ ヒープを使用する

デバッグ ヒープを使用するには、C ランタイム ライブラリのデバッグ バージョンと、アプリケーションのデバッグ ビルドをリンクします。malloc、free、calloc、realloc、new、delete などのヒープ関数に対する呼び出しはすべて、デバッグ ヒープで動作するこれらの関数のデバッグ バージョンに置き換えられます。メモリ ブロックを解放すると、デバッグ ヒープにより、割り当てられた領域のどちらか一方にあるバッファの完全性が自動的にチェックされ、上書きされている場合は、エラー レポートが発行されます。

デバッグ ヒープ機能の多くには、コードの内部でアクセスする必要があります。たとえば、_CrtCheckMemory に対する呼び出しを使用して、好きなときにヒープの完全性をチェックすることができます。この関数により、ヒープにあるメモリ ブロックがすべて調べられ、メモリ ブロックのヘッダー情報が有効であるかどうかが検証され、バッファが修正されていないことが確認されます。内部フラグ _crtDbgFlag を使用して、デバッグ ヒープが割り当てをトラックする方法を制御することができます。このフラグの読み込み、設定には _CrtSetDbgFlag 関数を使用します。このフラグを変更することにより、デバッグ ヒープに対して、プログラムが終了したときにメモリのリークをチェックし、リークが検知されたときは報垂キるように指示することができます。同様に、解放されたメモリ ブロックがリンクされたリストから削除されないように指定し、低メモリ状態をシミュレートすることもできます。ヒープのチェック時に、これらの解放されたブロックで混乱が発生していないことを保証するために、ブロック全体が調べられます。

_crtDbgFlag フラグには次のビット フィールドが含まれます。

ビット フィールド 説明
_CRTDBG_ALLOC_MEM_DF On デバッグ アロケーションをオンにします。このビットがオフの場合、複数のアロケーションはまとめてチェーンされたままになりますが、ブロック タイプは _IGNORE_BLOCK になります。
_CRTDBG_DELAY_FREE_MEM_DF Off 低メモリ状態をシミュレートするために、メモリが実際に解放されないようにします。このビットがオンの場合、解放されたブロックはデバッグ ヒープのリンクされたリストに残りますが、_FREE_BLOCK とマークされ、特別なバイト値でいっぱいにされます。
_CRTDBG_CHECK_ALWAYS_DF Off アロケーション、およびアロケーション解放のたびに、_CrtCheckMemory が呼び出されるようになります。この結果、実行は遅くなりますが、より速くエラーを検知できます。
_CRTDBG_CHECK_CRT_DF Off タイプ _CRT_BLOCK とマークされているブロックが、リーク検知操作、および状態の相違点操作に含まれるようになります。このビットがオフの場合、このような操作中、ランタイム ライブラリによって内部的に使用されるメモリは無視されます。
_CRTDBG_LEAK_CHECK_DF Off _CrtDumpMemoryLeaks を呼び出すことにより、プログラムの終了時に、リークのチェックが実行されるようになります。アプリケーションで、割り当てられたメモリすべてを解放できなかった場合、エラー レポートが生成されます。

これらのビット フィールドを変更し、フラグに対して新しい状態を作成するには、次の手順を実行します。

  1. newFlag パラメータを _CRTDBG_REPORT_FLAG にセットして、_CrtSetDbgFlag を呼び出し、現在の _crtDbgFlag の状態を取得し、返された値を一時変数に格納します。
  2. この一時変数と対応するビットマスクの OR (ビットごとの | 記号) を取り、ビットをオンにします。ビットマスクは、アプリケーション コードでは、記号定数で表されます。
  3. 適切なビットマスクの NOT (ビットごとの ~ 記号) と変数の AND (ビットごとの & 記号) を使用することにより、そのほかのビットをオフにします。
  4. 一時変数に格納された値に設定された newFlag パラメータを使って _CrtSetDbgFlag を呼び出し、_crtDbgFlag の新しい状態を作成します。

たとえば、次のコードでは、自動リーク検知がオンにされ、_CRT_BLOCK というブロックに対するチェックがオフにされます。

// 現在のフラグを取得します
int tmpFlag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);

// リーク検知用ビットをオンにします
tmpFlag |= _CRTDBG_LEAK_CHECK_DF;

// CRT ブロック チェック用ビットをオフにします
tmpFlag &= ~_CRTDBG_CHECK_CRT_DF;

// フラグに新しい値を設定します
_CrtSetDbgFlag(tmpFlag);

ヒープの状態を報告する関数

指定された瞬間のデバッグ ヒープの内容を報告するために、新しい関数がいくつか用意されています。指定された瞬間のヒープの状態を概要スナップショットに取り込むには、CRTDBG.h で定義されている _CrtMemState 構造体を使用します。

typedef struct _CrtMemState
{
// 一番最後に割り当てられたブロックへのポインタ
   struct _CrtMemBlockHeader * pBlockHeader;
// 5 種類のブロックそれぞれに対するカウンタ
   long lCounts[_MAX_BLOCKS];
// 各ブロック タイプに割り当てられたバイト数の合計
   long lSizes[_MAX_BLOCKS];
// これまで、一度に割り当てられた最も大きなバイト数
   long lHighWaterCount;
// 現在割り当てられているバイト数の合計
   long lTotalCount;
} _CrtMemState;

この構造体では、デバッグ ヒープのリンクされたリストにある最初のブロック (一番最後に割り当てられたブロック) へのポインタが保存されます。次に、_NORMAL_BLOCK、_CLIENT_BLOCK、_FREE_BLOCK などのメモリ ブロック タイプ別にリストにあるブロックの数と、これらのブロック タイプそれぞれに割り当てられたバイト数が 2 つの配列に記録されます。最後に、それまでに全体としてヒープに割り当てられた最大バイト数と、割り当てられたバイト数が記録されます。

以下の関数により、ヒープの状態と内容がレポートされます。これらの関数は情報を使用して、メモリのリークなどの問題を検知する手助けをします。

関数 説明
_CrtMemCheckpoint アプリケーションにより提供された _CrtMemState 構造体にあるヒープのスナップショットを保存します。
_CrtMemDifference 2 つのメモリ状態構造体を比較し、その差分を 3 つめの状態構造体に保存します。2 つの状態が異なっている場合は、TRUE が返されます。
_CrtMemDumpStatistics 指定された _CrtMemState 構造体をダンプします。この構造体には、指定された瞬間のデバッグ ヒープの状態を表すスナップショット、または 2 つのスナップショットの差異が含まれます。ダンプとは、人間が理解できる形でデータを報告することを意味します。
_CrtMemDumpAllObjectsSince ヒープで指定されたスナップショットが取られた後、または実行が開始されてから割り当てられたオブジェクトすべてに関する情報をダンプします。_CrtSetDumpClient を使用してフック関数がインストールされている場合、_CLIENT_BLOCK ブロックがダンプされるたびに、アプリケーションによって提供されるフック関数が呼び出されます。
_CrtDumpMemoryLeaks プログラムの実行開始後に、メモリのリークが発生しているかどうかを判別します。リークが発生している場合、割り当てられたオブジェクトがすべてダンプされます。_CrtSetDumpClient を使用してフック関数がインストールされている場合、_CLIENT_BLOCK ブロックがダンプされるたびに、アプリケーションによって提供されるフック関数が呼び出されます。

ヒープ割り当て要求をトラックする

アサート マクロ、またはレポート マクロが実行されるソース ファイル名と行番号を正確に示すと、問題の原因を突き止める際に便利であることがよくあります。ただし、ヒープ割り当て関数ではそうではありません。アプリケーションの論理ツリーにある多数の適切なポイントにマクロを挿入することができます。また、アロケーションはさまざまなところから、何回も呼び出される特殊なルーチンに埋もれていることがよくあります。問題はコードのどの行が悪い割り当てを行っているかではありません。コードのその行によって行われる無数のアロケーションのどれが誤りで、なぜ、そうなるのかが問題なのです。

誤りのある特定のヒープ割り当て呼び出しを識別するための最も単純な方法では、デバッグ ヒープで各ブロックに関連付けられている一意の割り当て要求番号を使用します。ダンプ関数によるブロックに関する情報のレポートでは、割り当て要求番号は {36} のように中かっこで囲まれています。

割り当てが適切に行われていないブロックの割り当て要求番号が判明したら、この番号を _CrtSetBreakAlloc に渡して、ブレークポイントを作成します。これにより、このブロックが割り当てられる前に、実行がブレークされ、誤った呼び出しの責任のあるルーチンを判断するために、さかのぼって調べることができます。再コンパイルを避けるには、_crtBreakAlloc を割り当て要求番号に設定して、この作業をデバッガで行います。

より複雑な方法としては、ヒープ割り当て関数の _dbg バージョンに匹敵する割り当てルーチンのデバッグ バージョンを作成するというものがあります。その後、基本となるヒープ割り当てルーチンにソース ファイルと行番号引数を渡し、どこで誤った割り当てが発生しているのかを即座につきとめることができます。

たとえば、アプリケーションに次のコードに類似した共用ルーチンが含まれているとします。

int addNewRecord(struct RecStruct * prevRecord,
                 int recType, int recAccess)
{
   /* ... 実際の割り当てで使用されるコードは省略します... */
   if ((newRec = malloc(recSize)) == NULL)
   /* ... 残りのルーチンも省略します... */
}

ヘッダー ファイルに、次のようなコードを追加します。

#ifdef _DEBUG
#define  addNewRecord(p, t, a) \
         addNewRecord(p, t, a, __FILE__, __LINE__)
#endif

次に、レコード作成ルーチンで割り当てを変更します。次に、レコード作成ルーチンで割り当てを変更します。

int addNewRecord(struct RecStruct *prevRecord,
                 int recType, int recAccess
#ifdef _DEBUG
               , const char *srcFile, int srcLine
#endif
  )
{
   /* ... 実際の割り当てで使用されるコードは省略します ... */
   if ((newRec = _malloc_dbg(recSize, _NORMAL_BLOCK,
        srcFile, scrLine)) == NULL)
   /* ... 残りのルーチンも省略します  ... */
}

これで、addNewRecord が呼び出された位置にあるソース ファイル名と行番号が、デバッグ ヒープに割り当てられた各結果ブロックに格納され、このブロックが調査されたときに報告が行われます。