MFCアプリが、メモリを書き壊した場所では止まらず、後のnewやdeleteで落ちることがあります。最初に置くのは、疑わしい処理の前後にある2つの_CrtCheckMemoryです。前がTRUE、後がFALSEなら、調べる範囲はその間に限れます。
最初に置く2つのチェックポイント
たとえば、注文データを読み込み、検索用インデックスを更新する処理の後で落ちるとします。落ちたdeleteを追う前に、業務処理の入口と出口を挟みます。
#include <crtdbg.h>
_ASSERTE(_CrtCheckMemory());
UpdateOrderCache();
_ASSERTE(_CrtCheckMemory());
_CrtCheckMemoryは、Debug CRTヒープが管理するブロックを検査します。正常ならTRUE、破壊を見つけるとFALSEを返します。_ASSERTEで囲めば、FALSEへ変わったチェックポイントでデバッガーを止められます。
| 処理前 | 処理後 | 次に調べる場所 |
|---|---|---|
TRUE | TRUE | その処理より後ろ |
TRUE | FALSE | 2つのチェックの間 |
FALSE | FALSE | 入口より前。呼び出し元へ戻る |
TRUEからFALSEへ変わった区間が調査範囲
検証サンプルでは、BuildIndexの中で32バイトの領域を確保し、境界の1バイト後ろへ書き込みます。実行結果は次の並びです。
[1] UpdateOrderCache() 直前 : TRUE
[2] LoadRecords() 後 : TRUE
[3] BuildIndex() 後 : FALSE
破壊区間: BuildIndex()
壊した行では例外が出ず、3番目の検査で初めて境界の上書きが見つかります。ここで調べる対象は、後から失敗するメモリ確保処理ではなく、2番目と3番目の間にあるBuildIndexです。

関数の中を半分ずつ挟み込む
関数まで絞れたら、その中の処理境界へチェックを移します。最初から全行へ置く必要はありません。前半と後半を分け、TRUEからFALSEへ変わった側だけをもう一度分けます。
void COrderCache::Update()
{
_ASSERTE(_CrtCheckMemory());
LoadRecords();
NormalizeKeys();
_ASSERTE(_CrtCheckMemory());
BuildIndex();
SaveSnapshot();
_ASSERTE(_CrtCheckMemory());
}
中央までがTRUEで、末尾がFALSEなら、次はBuildIndexとSaveSnapshotの間へ1つ追加します。同じ手順を繰り返すと、破壊の発生区間を関数単位、ループ単位へ狭められます。
再現条件が複雑な場合は、戻り値をログへ残す方法も使えます。ただし、FALSEを記録して処理を続けるのは調査用に限ります。破壊済みのヒープを使い続けることはできません。
const int heapOk = _CrtCheckMemory();
TRACE(L"BuildIndex after: %s\n", heapOk ? L"TRUE" : L"FALSE");
_ASSERTE(heapOk);
毎回のヒープ検査は再現区間を絞ってから
_CRTDBG_CHECK_ALWAYS_DFを有効にすると、メモリの割り当てと解放のたびに_CrtCheckMemoryが呼ばれます。破壊直後へ近づきやすい反面、診断対象でない処理にも検査が入ります。まず手動のチェックポイントで関数を絞り、その再現経路だけで有効にする順番が扱いやすいです。
int flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
flags |= _CRTDBG_CHECK_ALWAYS_DF;
_CrtSetDbgFlag(flags);
調査を終えたら、取得した現在値からフラグを外して戻します。
int flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
flags &= ~_CRTDBG_CHECK_ALWAYS_DF;
_CrtSetDbgFlag(flags);
検査対象はDebug CRTヒープの管理ブロック
_CrtCheckMemoryが確認するのは、Debug CRTヒープの基礎ヒープ、ブロックヘッダー、各ブロックの前後に置かれた境界バッファです。Microsoftの説明では、この境界バッファは現在4バイトで、既知の値0xFDが入ります。配列の末尾を越えて書くとこの領域が変わるため、次の検査で不整合として見つかります。
_DEBUGが未定義のビルドでは、_CrtCheckMemoryの呼び出しは前処理で取り除かれます。また、_CRTDBG_ALLOC_MEM_DFを無効にすると、検査せずTRUEを返します。Releaseだけで再現する問題や、Debug CRTを通らない独自アロケーターの破壊を、この関数だけで追うことはできません。
仕様の確認先はMicrosoft Learnの_CrtCheckMemory、_CrtSetDbgFlag、CRT debug heap detailsです。
- 疑わしい処理の前後へ
_CrtCheckMemoryを置きます。 TRUEからFALSEへ変わった間だけを分割します。_CRTDBG_CHECK_ALWAYS_DFは、再現区間を絞った後に使います。
