【MFC】_CrtCheckMemoryでメモリ破壊の発生区間を挟み込む

【MFC】_CrtCheckMemoryでメモリ破壊の発生区間を挟み込む

MFCアプリが、メモリを書き壊した場所では止まらず、後のnewdeleteで落ちることがあります。最初に置くのは、疑わしい処理の前後にある2つの_CrtCheckMemoryです。前がTRUE、後がFALSEなら、調べる範囲はその間に限れます。

目次

最初に置く2つのチェックポイント

たとえば、注文データを読み込み、検索用インデックスを更新する処理の後で落ちるとします。落ちたdeleteを追う前に、業務処理の入口と出口を挟みます。

#include <crtdbg.h>

_ASSERTE(_CrtCheckMemory());
UpdateOrderCache();
_ASSERTE(_CrtCheckMemory());

_CrtCheckMemoryは、Debug CRTヒープが管理するブロックを検査します。正常ならTRUE、破壊を見つけるとFALSEを返します。_ASSERTEで囲めば、FALSEへ変わったチェックポイントでデバッガーを止められます。

処理前処理後次に調べる場所
TRUETRUEその処理より後ろ
TRUEFALSE2つのチェックの間
FALSEFALSE入口より前。呼び出し元へ戻る

TRUEからFALSEへ変わった区間が調査範囲

検証サンプルでは、BuildIndexの中で32バイトの領域を確保し、境界の1バイト後ろへ書き込みます。実行結果は次の並びです。

[1] UpdateOrderCache() 直前 : TRUE
[2] LoadRecords() 後          : TRUE
[3] BuildIndex() 後           : FALSE

破壊区間: BuildIndex()

壊した行では例外が出ず、3番目の検査で初めて境界の上書きが見つかります。ここで調べる対象は、後から失敗するメモリ確保処理ではなく、2番目と3番目の間にあるBuildIndexです。

_CrtCheckMemoryの実行結果。UpdateOrderCache直前とLoadRecords後はTRUE、BuildIndex後はFALSEとなり、破壊区間がBuildIndexと表示されている

関数の中を半分ずつ挟み込む

関数まで絞れたら、その中の処理境界へチェックを移します。最初から全行へ置く必要はありません。前半と後半を分け、TRUEからFALSEへ変わった側だけをもう一度分けます。

void COrderCache::Update()
{
    _ASSERTE(_CrtCheckMemory());

    LoadRecords();
    NormalizeKeys();

    _ASSERTE(_CrtCheckMemory());

    BuildIndex();
    SaveSnapshot();

    _ASSERTE(_CrtCheckMemory());
}

中央までがTRUEで、末尾がFALSEなら、次はBuildIndexSaveSnapshotの間へ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_CrtSetDbgFlagCRT debug heap detailsです。

  • 疑わしい処理の前後へ_CrtCheckMemoryを置きます。
  • TRUEからFALSEへ変わった間だけを分割します。
  • _CRTDBG_CHECK_ALWAYS_DFは、再現区間を絞った後に使います。
目次