CRT デバッグ ヒープの詳細
CRT デバッグ ヒープと関連関数は、コード内のメモリ管理の問題を追跡およびデバッグするためのさまざまな方法を提供します。 これを使用して、バッファー オーバーランを見つけ、メモリ割り当てとメモリの状態を追跡して報告することができます。 また、独自のアプリのニーズに合わせて独自のデバッグ割り当て関数を作成することもできます。
デバッグ ヒープを使用してバッファー オーバーランを見つける
プログラマが遭遇する最も一般的で難しい問題の 2 つは、割り当てられたバッファーとメモリ リークの末尾を上書きすることです (不要になった後に割り当てを解放できない)。 デバッグ ヒープは、このようなメモリ割り当ての問題を解決するための強力なツールとなります。
デバッグ バージョンのヒープ関数は、リリース ビルドで使用される標準バージョン (基本バージョン) の関数を呼び出します。 メモリ ブロックを要求すると、デバッグ ヒープ マネージャーは、要求したよりも少し大きいメモリ ブロックをベース ヒープから割り当て、そのブロックの部分へのポインターを返します。 たとえば、アプリケーション中で malloc( 10 )
という呼び出しをしたとします。 リリース ビルドでは、 malloc
は 10 バイトの割り当てを要求するベース ヒープ割り当てルーチンを呼び出します。 ただし、デバッグ ビルドでは、 malloc
は _malloc_dbg
を呼び出し、10 バイトと約 36 バイトの余分なメモリの割り当てを要求するベース ヒープ割り当てルーチンを呼び出します。 デバッグ ヒープに割り当てられたすべてのメモリ ブロックは、単一のリンク リストとして、割り当てられた順番で連結されます。
デバッグ ヒープ ルーチンによって割り当てられる余分なメモリは、ブックキーピング情報に使用されます。 これには、デバッグ メモリ ブロックを一緒にリンクするポインターと、割り当てられたリージョンの上書きをキャッチするデータの両側の小さなバッファーがあります。
現在、デバッグ ヒープのブックキーピング情報を格納するために使用されるブロック ヘッダー構造は、 <crtdbg.h>
ヘッダーで宣言され、 <debug_heap.cpp>
CRT ソース ファイルで定義されています。 概念的には、次の構造に似ています。
typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
_CrtMemBlockHeader* _block_header_next;
// Pointer to the block allocated just after this one:
_CrtMemBlockHeader* _block_header_prev;
char const* _file_name;
int _line_number;
int _block_use; // Type of block
size_t _data_size; // Size of user block
long _request_number; // Allocation number
// Buffer just before (lower than) the user's memory:
unsigned char _gap[no_mans_land_size];
// Followed by:
// unsigned char _data[_data_size];
// unsigned char _another_gap[no_mans_land_size];
} _CrtMemBlockHeader;
ブロックのユーザー データ領域の両側にある no_mans_land
バッファーは、現在 4 バイトのサイズであり、デバッグ ヒープ ルーチンによって使用される既知のバイト値でいっぱいになり、ユーザーのメモリ ブロックの制限が上書きされていないことを確認します。 デバッグ ヒープは、新しく確保されたメモリ ブロックにも既知の値を格納します。 解放されたブロックをヒープのリンク リストに保持することを選択した場合、これらの解放されたブロックにも既知の値が入力されます。 現在、実際に使用されているバイト値は次のとおりです。
no_mans_land
(0xFD)
現在、アプリケーションによって使用されるメモリの両側にある "no_mans_land" バッファーには、0xFDが格納されています。
解放済みのブロック (0xDD)
_CRTDBG_DELAY_FREE_MEM_DF
フラグが設定されている場合、解放済みのブロックは使用されずにヒープのリンク リストに保持され、0xDD が格納されます。
新規オブジェクト (0xCD)
新しいオブジェクトは、割り当てられると0xCDでいっぱいになります。
デバッグ ヒープ上のメモリ ブロックの型
デバッグ ヒープ上のすべてのメモリ ブロックには、5 つの割り当て型のうちのいずれかの型が割り当てられます。 型によって、メモリ リークの検出やメモリ状態をレポートするときの追跡方法やレポート方法が異なります。 ブロックの型は、 _malloc_dbg
などのデバッグ ヒープ割り当て関数の 1 つへの直接呼び出しを使用して割り当てることで指定できます。 デバッグ ヒープ内の 5 種類のメモリ ブロック (_CrtMemBlockHeader
構造体のnBlockUse
メンバーに設定) は次のとおりです。
_NORMAL_BLOCK
malloc
またはcalloc
を呼び出すと、標準ブロックが作成されます。 Normal ブロックのみを使用し、クライアント ブロックが不要な場合は、 _CRTDBG_MAP_ALLOC
を定義できます。 _CRTDBG_MAP_ALLOC
では、すべてのヒープ割り当て呼び出しがデバッグ ビルドで同等のデバッグにマップされます。 これにより、各割り当て呼び出しに関するファイル名と行番号情報を、対応するブロック ヘッダーに格納できます。
_CRT_BLOCK
多数のランタイム ライブラリ関数は、内部的にメモリ ブロックを割り当てます。このように内部的に割り当てられたメモリ ブロックは CRT ブロックとして個別に扱われます。 その結果、リーク検出やその他の操作は影響を受けない可能性があります。 CRT 型のブロックが、割り当て関数によって割り当てられたり、再割り当てされたり、解放されたりすることはありません。
_CLIENT_BLOCK
アプリケーションでは、デバッグ ヒープ関数を明示的に呼び出し、このメモリ ブロック型を割り当てることによって、特定の割り当て領域をデバッグの目的で特別に追跡できます。 たとえば、MFC では、すべての CObject
オブジェクトがクライアント ブロックとして割り当てられます。他のアプリケーションでは、クライアント ブロック内に異なるメモリ オブジェクトが保持される場合があります。 より細かい追跡を実施するために、Client ブロック型を細分化することもできます。 Client ブロック型を細分化した型を指定するには、その型を表す数値を左に 16 ビットシフトし、OR
との _CLIENT_BLOCK
演算を行います。 次に例を示します。
#define MYSUBTYPE 4
freedbg(pbData, _CLIENT_BLOCK|(MYSUBTYPE<<16));
クライアント ブロックに格納されているオブジェクトをダンプするためのクライアント提供のフック関数は、 _CrtSetDumpClient
を使用してインストールでき、デバッグ関数によってクライアント ブロックがダンプされるたびに呼び出されます。 また、 _CrtDoForAllClientObjects
を使用して、デバッグ ヒープ内のすべてのクライアント ブロックに対してアプリケーションによって提供される特定の関数を呼び出すことができます。
_FREE_BLOCK
通常は解放済みのブロックはリストから削除されます。 解放されたメモリが書き込まれていないことを確認したり、メモリ不足の状態をシミュレートしたりするには、解放されたブロックをリンク リストに保持し、Free としてマークし、既知のバイト値 (現在0xDD) を入力します。
_IGNORE_BLOCK
デバッグ ヒープ操作を一定の間隔でオフにすることができます。 この間は、メモリ ブロックはリスト中に保持されますが、Ignore ブロックとしてマークされます。
特定のブロックの型とサブタイプを確認するには、関数 _CrtReportBlockType
とマクロの _BLOCK_TYPE
と _BLOCK_SUBTYPE
を使用します。 マクロは、次のように <crtdbg.h>
で定義されます。
#define _BLOCK_TYPE(block) (block & 0xFFFF)
#define _BLOCK_SUBTYPE(block) (block >> 16 & 0xFFFF)
ヒープの整合性とメモリ リークを調べる
デバッグ ヒープ用の機能の多くは、コードの中からアクセスする必要があります。 次のセクションでは、これらの機能のいくつかについて、使用方法を説明します。
_CrtCheckMemory
呼び出しを使用して _CrtCheckMemory
できます。たとえば、任意の時点でヒープの整合性を確認できます。 この関数は、ヒープ内のすべてのメモリ ブロックを検査します。 メモリ ブロック ヘッダー情報が有効であることを確認し、バッファーが変更されていないことを確認します。
_CrtSetDbgFlag
_CrtSetDbgFlag
関数を使用して読み取りおよび設定できる内部フラグ_crtDbgFlag
を使用して、デバッグ ヒープが割り当てを追跡する方法を制御できます。 このフラグを変更することによって、デバッグ ヒープを使用して、プログラムの終了時にメモリ リークをチェックし、検出されたメモリ リークをすべて出力できます。 同様に、メモリ不足の状況をシミュレートするために、解放されたメモリ ブロックをリンク リストに残すようにヒープに指示できます。 ヒープがチェックされると、これらの解放されたブロックが完全に検査され、それらが妨げられないことを確認します。
_crtDbgFlag
フラグには、次のビット フィールドが含まれています。
ビット フィールド | 規定値 | 説明 |
---|---|---|
_CRTDBG_ALLOC_MEM_DF |
オン | デバッグ用の割り当てをオンにします。 このビットがオフの場合、割り当てはチェーンされたままになりますが、ブロックの種類は _IGNORE_BLOCK 。 |
_CRTDBG_DELAY_FREE_MEM_DF |
"オフ" | メモリ不足の状態をシミュレートできるように、実際にはメモリを解放しません。 このビットがオンの場合、解放されたブロックはデバッグ ヒープのリンク リストに保持されますが、 _FREE_BLOCK としてマークされ、特殊なバイト値が入力されます。 |
_CRTDBG_CHECK_ALWAYS_DF |
"オフ" | 割り当てと割り当て解除のたびに _CrtCheckMemory が呼び出されます。 実行は遅くなりますが、エラーをすばやくキャッチします。 |
_CRTDBG_CHECK_CRT_DF |
"オフ" | タイプ _CRT_BLOCK としてマークされたブロックをリーク検出および状態差操作に含めます。 このビットがオフの場合、ランタイム ライブラリによって内部的に使用されるメモリは、これらの処理対象には含まれません。 |
_CRTDBG_LEAK_CHECK_DF |
"オフ" | _CrtDumpMemoryLeaks の呼び出しを介してプログラムの終了時にリーク チェックを実行します。 アプリケーションが割り当てたメモリを解放できていない場合は、エラー レポートが生成されます。 |
デバッグ ヒープを構成する
malloc
、free
、calloc
、realloc
、new
、delete
などのヒープ関数の呼び出しは、すべてデバッグ ヒープで動作するデバッグ バージョンのヒープ関数に置き換えられます。 メモリ ブロックが解放されると、デバッグ ヒープは割り当て領域の前後に確保されたバッファーが完全かどうかを自動的にチェックし、それらのバッファーが上書きされていた場合はエラーを出力します。
デバッグ ヒープを使用するには
- アプリケーションのデバッグ ビルドを C ランタイム ライブラリのデバッグ バージョンにリンクします。
1 つ以上の _crtDbgFlag
ビット フィールドを変更し、フラグの新しい状態を作成するには
_CrtSetDbgFlag
パラメーターにnewFlag
を設定して_CRTDBG_REPORT_FLAG
を呼び出すことによって、現在の_crtDbgFlag
の状態を取得し、その値を一時変数に格納します。対応するビットマスク (マニフェスト定数によってアプリケーション コードで表される) を持つ一時変数に対してビットごとの
|
演算子 ("または") を使用して、任意のビットを有効にします。適切なビットマスクのビットごとの
~
演算子 ("not"または補数) を持つ変数に対してビットごとの&
演算子 ("and") を使用して、他のビットをオフにします。_CrtSetDbgFlag
パラメーターに一時変数に格納されている値を設定してnewFlag
を呼び出すことによって、_crtDbgFlag
の新しい状態を作成します。たとえば、次のコード行は、自動リーク検出を有効にし、
_CRT_BLOCK
型のブロックのチェックを無効にします。// Get current flag int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG ); // Turn on leak-checking bit. tmpFlag |= _CRTDBG_LEAK_CHECK_DF; // Turn off CRT block checking bit. tmpFlag &= ~_CRTDBG_CHECK_CRT_DF; // Set flag to the new value. _CrtSetDbgFlag( tmpFlag );
new
、 delete
、および C++ デバッグ ヒープ内の _CLIENT_BLOCK
割り当て
C ランタイム ライブラリのデバッグ バージョンには、C++ の new
演算子と delete
演算子のデバッグ バージョンが含まれています。 _CLIENT_BLOCK
型を割り当てる場合は、次の例に示すように、new
演算子のデバッグ バージョンを直接呼び出すか、デバッグ モードで new
演算子を置き換えるマクロを作成する必要があります。
/* MyDbgNew.h
Defines global operator new to allocate from
client blocks
*/
#ifdef _DEBUG
#define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define DEBUG_CLIENTBLOCK
#endif // _DEBUG
/* MyApp.cpp
Use a default workspace for a Console Application to
* build a Debug version of this code
*/
#include "crtdbg.h"
#include "mydbgnew.h"
#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif
int main( ) {
char *p1;
p1 = new char[40];
_CrtMemDumpAllObjectsSince( NULL );
}
デバッグ バージョンの delete
演算子は、すべてのブロック型に対して使用できるため、リリース バージョンのコンパイル時にプログラムを変更する必要はありません。
ヒープ状態レポート関数
特定の時点でのヒープの状態の概要スナップショットをキャプチャするには、<crtdbg.h>
で定義されている_CrtMemState
構造体を使用します。
typedef struct _CrtMemState
{
// Pointer to the most recently allocated block:
struct _CrtMemBlockHeader * pBlockHeader;
// A counter for each of the 5 types of block:
size_t lCounts[_MAX_BLOCKS];
// Total bytes allocated in each block type:
size_t lSizes[_MAX_BLOCKS];
// The most bytes allocated at a time up to now:
size_t lHighWaterCount;
// The total bytes allocated at present:
size_t lTotalCount;
} _CrtMemState;
この構造体には、デバッグ ヒープのリンク リストの先頭 (最近割り当てられた) ブロックへのポインターが格納されます。 次に、2 つの配列に、各種類のメモリ ブロック (_NORMAL_BLOCK
、 _CLIENT_BLOCK
、 _FREE_BLOCK
など) の数と、各ブロックの種類に割り当てられたバイト数が記録されます。 最後に、その時点までにヒープに割り当てられたバイト数の最大値と、現在割り当てられているバイト数が格納されています。
その他の CRT レポート関数
ヒープの状態と内容をレポートするための関数を次に示します。これらの関数によって取得した情報を利用して、メモリ リークなどの問題を検出できます。
関数 | 説明 |
---|---|
_CrtMemCheckpoint |
ヒープのスナップショットを、アプリケーションによって提供される _CrtMemState 構造に保存します。 |
_CrtMemDifference |
メモリ状態を格納した 2 つの構造体を比較し、その相違点を別の構造体に保存します。2 つの状態が異なっている場合は TRUE を返します。 |
_CrtMemDumpStatistics |
特定の _CrtMemState 構造体をダンプします。 この構造体には、ある時点でのデバッグ ヒープの状態のスナップショット、または 2 つのスナップショットの相違点が格納されています。 |
_CrtMemDumpAllObjectsSince |
指定されたスナップショットの取得以降、またはプログラムの実行開始以降に割り当てられたすべてのオブジェクトに関する情報をダンプします。 _CLIENT_BLOCK ブロックをダンプするたびに、アプリケーションによって提供されるフック関数が呼び出されます (_CrtSetDumpClient を使用してインストールされている場合)。 |
_CrtDumpMemoryLeaks |
プログラムの実行開始以降にメモリ リークが発生したかどうかを調べます。メモリ リークが発生している場合は、割り当てられている全オブジェクトをダンプします。 _CrtDumpMemoryLeaks _CLIENT_BLOCK ブロックをダンプするたびに、アプリケーションによって提供されるフック関数が呼び出されます (_CrtSetDumpClient を使用してインストールされている場合)。 |
ヒープ割り当て要求を追跡する
アサート マクロまたはレポート マクロのソース ファイル名と行番号を知ることは、多くの場合、問題の原因を特定するのに役立ちます。 ヒープ割り当て関数に当てはまる可能性は高くありません。 アプリケーションのロジック ツリー内の多くの適切なポイントにマクロを挿入できますが、多くの場合、さまざまな場所から呼び出される関数に割り当てが埋め込まれます。 問題は、どのコード行が不適切な割り当てを行ったかではありません。 代わりに、そのコード行によって行われた何千もの割り当ての 1 つが悪かったのと、その理由です。
一意の割り当て要求番号と _crtBreakAlloc
問題が発生した特定のヒープ割り当て呼び出しを識別する簡単な方法があります。 デバッグ ヒープ内の各ブロックに関連付けられている一意の割り当て要求番号を利用します。 ダンプ関数を使用してブロックの情報を出力すると、この割り当て要求番号が中かっこで囲まれて表示されます ("{36}" など)。
不適切に割り当てられたブロックの割り当て要求番号がわかったら、この番号を _CrtSetBreakAlloc
に渡してブレークポイントを作成できます。 このブロックが割り当てられる直前でプログラムの実行が停止するため、その時点からさかのぼって、不正な呼び出しの原因となっているルーチンを突き止めることができます。 再コンパイルを回避するには、関心のある割り当て要求番号に _crtBreakAlloc
を設定することで、デバッガーで同じことを実行できます。
割り当てルーチンのデバッグ バージョンの作成
より複雑なアプローチは、独自の割り当てルーチンのデバッグ バージョンを作成することです。これは、heap 割り当て関数の_dbg
バージョンと同等です。 その後、ソース ファイルと行番号の引数を基になるヒープ割り当てルーチンに渡すと、正しくない割り当てが発生した場所をすぐに確認できます。
たとえば、アプリケーションに、次の例のような一般的に使用されるルーチンが含まれているとします。
int addNewRecord(struct RecStruct * prevRecord,
int recType, int recAccess)
{
// ...code omitted through actual allocation...
if ((newRec = malloc(recSize)) == NULL)
// ... rest of routine omitted too ...
}
ヘッダー ファイルには、次の例のようなコードを追加できます。
#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
)
{
/* ... code omitted through actual allocation ... */
if ((newRec = _malloc_dbg(recSize, _NORMAL_BLOCK,
srcFile, scrLine)) == NULL)
/* ... rest of routine omitted too ... */
}
これで、addNewRecord
が呼び出された場所のソース ファイル名と行番号が、デバッグ ヒープ上に割り当てられた各ブロックに格納されるため、このブロックを調べれば、これらの情報を出力できます。