Visual Studio でデバッグ実行していたMFCアプリが止まり、出力ウィンドウに0xC0000005という数字だけが残る。よくある現象ですよね。原因はこの1行から絞れます。その行に出る読み取り中か書き込み中かと、直前のアドレス値の2つを最初に読めば、NULLポインタ・未初期化・解放済み・メモリ破壊のどこに問題があるのかが分かります。
0xC0000005 の行に出る2つの数字と、その読み方
0xC0000005 は、スレッドが権限のない仮想アドレスを読もう・書こうとしたときに Windows が上げる例外で、SDK ではEXCEPTION_ACCESS_VIOLATIONという名前が付いています。デバッガの下で起きると、出力ウィンドウには次の1行が出ます。
0x00007FF6A1B2C3D4 で例外がスローされました (MyApp.exe 内): 0xC0000005: 場所 0x0000000000000000 の読み取り中にアクセス違反が発生しました
この行の情報は、例外を記録するEXCEPTION_RECORD 構造体そのものです。公式の説明では、アクセス違反のときExceptionInformation[0]は操作の種類を表し、0 なら読み取り、1 なら書き込み、8 ならユーザーモードの DEP 違反、ExceptionInformation[1]は触れなかった仮想アドレスと決まっています。出力ウィンドウの1行では、「場所 … の読み取り中」なら読もうとして落ちた、「場所 … への書き込み中」なら書こうとして落ちた、と読めます。
読み取りで落ちたなら、そのポインタは値を取り出そうとした先です。書き込みで落ちたなら、代入先か、メンバー関数の中で自分の状態を更新した瞬間が怪しいです。同じ2つの値は、構造化例外ハンドラーからも取り出せます。次のフィルタ関数は、この記事のために作った実機サンプルからそのまま引いています(Debug / x64 でビルドし、実行して6つのケースを再現したものです)。
// 例外情報から「read か write か」と「触ろうとしたアドレス」を取り出す。
LONG AvCaptureFilter(EXCEPTION_POINTERS* pPointers, AV_RAW_INFO* pInfo)
{
if (pPointers == NULL || pInfo == NULL)
return EXCEPTION_CONTINUE_SEARCH;
const EXCEPTION_RECORD* pRecord = pPointers->ExceptionRecord;
if (pRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION)
{
// アクセス違反以外はここで扱わない。上位のハンドラへ渡す。
return EXCEPTION_CONTINUE_SEARCH;
}
// [0] は 0=読み取り / 1=書き込み / 8=DEP 違反、[1] は触れなかったアドレス。
pInfo->bCaught = TRUE;
pInfo->nExceptionCode = pRecord->ExceptionCode;
pInfo->nParameterCount = pRecord->NumberParameters;
pInfo->nOperation = (pRecord->NumberParameters >= 2) ? pRecord->ExceptionInformation[0] : 0;
pInfo->nAddress = (pRecord->NumberParameters >= 2) ? pRecord->ExceptionInformation[1] : 0;
pInfo->pExceptionAddress = pRecord->ExceptionAddress;
return EXCEPTION_EXECUTE_HANDLER;
}
NumberParametersを確かめてからExceptionInformationを読んでいるのは、この2つが揃っていない例外では操作もアドレスも取れないためです。
デバッガを付けられない現地環境では、この2つの値をログに残しておくと、後から同じ絞り込みができます。
アドレス値から原因を絞る早見表
デバッグビルドでは、確保直後のメモリと解放したメモリが決まったバイト値で埋められます。そのため、読めなかった番地の見た目から原因の見当が付きます。
| 見えた値 | まず疑うもの | 次に見るところ |
|---|---|---|
アドレス欄が0x0000000000000000 | NULL のポインタをそのまま参照した | 直前にポインタを受け取った関数の戻り値 |
アドレス欄が0x0000000000000040のような小さい値 | NULL のオブジェクトのメンバーに触った | そのポインタを取得した場所と、失敗時の分岐 |
ポインタの値が0xCDCDCDCD(x64 のアドレス欄は0xFFFF…FF) | 初期化していないポインタ(新規ブロックの埋め値 0xCD) | コンストラクタでの初期化漏れ |
ポインタの値が0xDDDDDDDD(x64 のアドレス欄は0xFFFF…FF) | 解放済みブロックの埋め値 0xDD を読んだ | delete のあとで使い続けていないか |
ポインタの値が0xFEEEFEEE | 解放済みのヒープ領域 | 同上 |
ポインタの値が0xCCCCCCCC | 未初期化のスタック変数 | ローカル変数の初期化 |
| 操作が 8(アドレス欄は実行しようとした番地) | 実行できない領域のコードを実行した | 関数ポインタの作り方と、データ用に確保した領域を実行していないか |
| アドレス欄が実行のたびに変わる大きな値 | スタックやヒープの破壊 | 直前に書き込んだバッファの長さ |
0xCD / 0xDD / 0xFD は CRT デバッグヒープの埋め値として公式に説明があります(出典は次節)。
0xFEEEFEEE と 0xCCCCCCCC は広く知られていますが、公式ドキュメントで記述を見つけられませんでした。最終行の「実行のたびに変わる大きな値」も、資料と突き合わせていない経験則です。
この表のうち、上から4行と操作が 8 の行は、実際に落ちるサンプルで確かめました。6つのケースを Debug / x64 で順に実行した結果が次の画面です。

0x0000000000000000でも操作が読み取り(0)と書き込み(1)に分かれます。3の0x0000000000000040はゼロに近い小さな値で、この構成でのCWnd::m_hWndの位置です。4と5では、報告されたアドレスが0xFFFFFFFFFFFFFFFFになりました。埋め値がそのままアドレス欄に出るとはかぎりません。この構成では、埋め値はアドレス欄ではなくポインタ自体の値に残っていました。x64 で早見表の0xCDCDCDCDと0xDDDDDDDDの行を使うときは、アドレス欄ではなく、ウォッチウィンドウで見たポインタの値と読み替えてください。
6件目は操作が 8、つまり DEP 違反でした。読み書きだけを許して確保した領域に命令を置き、そこへ関数ポインタで飛んだケースです。他の5件と違うのは、触れなかったアドレスと、例外を起こした命令の番地が同じ値になったこと、そしてその番地がどのモジュールにも属していないことでした。操作が 8 で出たときは、データ用に確保した領域を実行していないか、関数ポインタの作り方が正しいかを見ます。
0x00000000 に近い小さな値だったとき
0x0000000000000040のような小さな値は、「NULL のオブジェクトの、先頭から 0x40 バイト目のメンバー」に触った跡です。上の実行画面の3件目がこれで、この構成ではCWnd::m_hWndがその位置にありました。オフセットはクラスと構成で変わるので、数字そのものを覚える必要はありません。「ゼロに近い小さな値ならメンバーのオフセット」とだけ頭に入れておきます。x64 では先頭の1ページ分が誰にも割り当てられないため、この範囲のアドレスはオフセットとして読めます。値が小さいほど、クラスの手前側のメンバーです。
MFC では、ウィンドウやドキュメントを取り出す関数の戻り値を確かめずに使うと、この形になります。GetDlgItemはコントロールが作られる前や ID を間違えたときに NULL を返し、GetParentやGetDocumentも文脈によっては NULL になります。
static void __cdecl FaultNullMember(void* pContext)
{
FAULT_CONTEXT* pFault = static_cast<FAULT_CONTEXT*>(pContext);
// GetDlgItem は、その ID のコントロールが無ければ NULL を返す。戻り値を
// 確かめずに使うと、落ちるのは呼び出し側ではなくメンバー関数の中になる。
// 触れなかったアドレスは 0 ではなく、メンバーのオフセットぶんの小さな値。
//
// ここで SetWindowText を使わないのは、MFC の CWnd::SetWindowText だけが
// 先頭に ENSURE(this) を持っているため(winocc.cpp)。NULL の this は
// アクセス違反ではなくアサートで先に捕まり、この現象を再現できない。
// ASSERT(::IsWindow(m_hWnd)) しか持たない EnableWindow を使うと、
// m_hWnd の読み取りでそのままアクセス違反になる。
CWnd* pMissing = pFault->pOwner->GetDlgItem(IDC_MISSING_CONTROL);
pFault->nUsedPointer = reinterpret_cast<ULONG_PTR>(pMissing);
pFault->bHasUsedPointer = TRUE;
pMissing->EnableWindow(FALSE);
}
途中の2行はサンプルが結果表示のためにポインタの値を控えているだけで、落ちるのは最後の1行です。同じ経路でも、受け取った直後に判定を入れると何も起きません。
static CString FixedNullMember(CWnd* pOwner)
{
CWnd* pMissing = pOwner->GetDlgItem(IDC_MISSING_CONTROL);
if (pMissing == NULL)
return _T("中止しました(GetDlgItem が NULL を返したので何もしていない)");
pMissing->EnableWindow(FALSE);
return _T("コントロールの状態を変更しました");
}
止まる行は、呼び出し側から1つ内側、メンバー関数の中になります。仮想でないメンバー関数は、規格上は未定義動作であるものの、実際には NULL のままthisとして渡されて呼び出しに成功し、メンバーへ触れた瞬間に落ちます。NULL チェックが抜けているコードは、落ちた行ではなく1つ下のフレームにあります。nullptrを使っていても、機械語の上では同じゼロなので現れ方は変わりません。
どの関数を呼んだかで止まり方が変わります。CWnd::EnableWindowは先頭がASSERT(::IsWindow(m_hWnd) || (m_pCtrlSite != NULL))で、m_hWndを読んだ瞬間にアクセス違反になります。実測でも、3件目で例外を起こした命令はmfc140ud.dllの中にありました。いっぽうCWnd::SetWindowTextは先頭がENSURE(this)で、NULL のthisはそこで捕まります。この場合は Debug Assertion Failed のダイアログが出て、0xC0000005 は出ません。どちらも Visual Studio に付属する MFC のソース(atlmfc\src\mfc\winocc.cpp)で確かめられます。
0xCDCDCDCD や 0xFEEEFEEE が出たとき
デバッグビルドの CRT は、確保したメモリと解放したメモリを決まった値で埋めます。CRT デバッグヒープの説明には、新しく確保したブロックは 0xCD、リストに残した解放済みブロックは 0xDD、確保領域の両側に置く番兵は 0xFD で埋める、と書かれています。0xCDCDCDCDを触ったのなら、そのポインタは「確保はされたが、まだ何も入れていないメモリ」から読んだ値です。使った場所より先に、コンストラクタや初期化処理を見ます。
0xFEEEFEEEは、デバッガの下で解放したヒープ領域に現れる値として広く知られています。ただし Microsoft の公開ドキュメントには定義が見当たりませんでした。断定はできませんが、この値が出たら解放済みの領域を触った可能性が高いと考えて、delete したポインタを使い続けていないかを確かめます。サンプルの対策側はこう書いてあります(ReadFirstByteは、NULL なら FALSE を返す共通の判定です)。
static CString FixedFreedBlock()
{
BYTE* pBlock = new BYTE[64];
::memset(pBlock, 0, 64);
delete[] pBlock;
// 解放したら、そのポインタに NULL を入れる。以後の誤用は 0xDD ではなく
// ゼロ番地の参照として現れるので、原因の読み違いが起きない。
pBlock = NULL;
int nValue = 0;
if (!ReadFirstByte(pBlock, &nValue))
return _T("中止しました(解放時に NULL を入れ、使う前の判定で止まった)");
CString sText;
sText.Format(_T("読み取りました(%d)"), nValue);
return sText;
}
解放したポインタに NULL を入れておくと、次の誤用は必ずゼロ番地になります。原因は同じでも、アドレスがゼロなら読み方に迷いません。アドレスが実行のたびに変わる大きな値なら、メモリの破壊を疑います。書き込み側を探す道具として、Run-Time Check Failure #2 の読み方と_CrtCheckMemory の挟み込みが使えます。
落ちた場所がライブラリの中だったとき
止まった行がmfc140ud.dll(デバッグビルドの MFC 本体)やntdll.dllの中でも、渡した引数やオブジェクトの状態を作ったのは自分のコードです。呼び出し履歴ウィンドウ(デバッグ > ウィンドウ > 呼び出し履歴、Ctrl+Alt+C)を開き、上から順に自分のソースが出るフレームまで降ります。
- フレームが
[<External Code>]にまとめられているときは、ツールバーまたは右クリックの「外部コードの表示」で展開します。 [Frames below may be incorrect and/or missing, no symbols loaded for name.dll]と出たフレームは、シンボルが無いために順序が信用できません。右クリックの「シンボルの読み込み」で読み込みます。この2つはメニュー項目と違い、Microsoft の日本語ドキュメントでも英語表記のまま載っている文字列です。- 自分のソースが出たフレームをダブルクリックすると、そのフレームのローカル変数が見えます。ここでポインタの値を見れば、早見表のどの行に当たるかが決まります。
切り替えても実行位置は動かず、続行やステップ実行は元のフレームから進みます。値を見て回っても状態は壊れません。フレームの読み方はCall Stack(呼び出し履歴)の歩き方で扱っています。
落ちた瞬間の変数が残っていないとき
自分で__exceptを書いていたり、途中のライブラリが例外を処理していたりすると、デバッガが止まるのは処理が終わったあとで、落ちた瞬間の変数は残っていません。例外設定ウィンドウ(デバッグ > ウィンドウ > 例外設定)で対象にチェックを入れると、公式の説明どおり、処理されるかどうかに関係なく、スローされた時点で実行が中断されます。これが最初のチャンス例外と呼ばれる止まり方です。ネイティブのアクセス違反は Win32 例外のカテゴリで探します。
この設定はソリューションの .suo ファイルに保存されるため、別のソリューションには引き継がれません。落ちる場所が毎回違うときや、処理された後でしか止まらないときは、先にこのチェックを入れてから再現させます。
なお Release ビルドでは、埋め値も行番号も期待できず、最適化で変数が消えていることもあります。Release でだけ落ちる場合の進め方はReleaseビルドだけ落ちるときの確認手順にまとめています。
まとめ
- 0xC0000005 の行では、読み取り中か書き込み中かと、その直前のアドレス値を先に読みます。公式仕様での対応は、読み取りが 0、書き込みが 1、DEP 違反が 8 です。
- ゼロ番地に近い小さな値は、NULL のオブジェクトのメンバー位置です。落ちた行はメンバー関数の内側でも、直すのは1つ下のフレームです。
- 0xCDCDCDCD は初期化前、0xDDDDDDDD は解放済みブロックの埋め値です。解放したポインタに NULL を入れておくと、次の誤用がゼロ番地になって読み違えません。
- 操作が 8 のときは読み書きではなく実行の拒否です。アドレス欄と、例外を起こした命令の番地が同じなら、データ用に確保した領域を実行しています。
- ハンドラーに飲まれて落ちた瞬間を見られないときは、例外設定でアクセス違反にチェックを入れ、最初のチャンス例外で止めます。
