MFCアプリがアクセス違反で止まったとき、だいたいはその行あたりに問題があり、そこを中心に見れば普通は片付きます。でも、渡された値がもう壊れていたときは、その行を何度読んでも答えが出ません。
そんなときはまずCall Stack(呼び出し履歴)を開きましょう。
停止した関数から呼び出し元へ1段ずつ遡れば、値が壊れた場所を特定できます。
Call Stack (呼び出し履歴) が積み上がる仕組み
プログラムでは、関数から別の関数を呼ぶたびに、呼び出し元の情報が順に積み重なっていきます。Call Stackウィンドウを見ると、今いる関数がどこから呼ばれてきたのかを順に確認できます。
一番上の行が「現在停止している関数」であり、下に行くほど「過去(呼び出し元)」に遡ります。いま実行が置かれているフレームには矢印が付くので、どれが現在地かはひと目で分かります。
開く手順
- デバッグ中に停止した状態にします。ブレークポイントでもクラッシュでも構いません。
- メニューの「デバッグ」→「ウィンドウ」→「呼び出し履歴」を選びます。ショートカットはCtrl + Alt + Cです。
このメニューはデバッグ中しか使えません。先に止めてから開いてください。
止まった行だけでは原因が分からない例
ここからは、注文の一覧と詳細が出るだけの画面を例にします。以降の手順と画面はVisual Studio 2026 / Debug / x64 / Unicodeで確認しました。MFCはダイアログベース、共有DLLの構成です。
一覧は注文番号・顧客名・状態の3列で、A-1045からA-1042までの4件が並びます。A-1042はその1行の注文番号です。この注文の詳細データだけ、顧客名のポインタを壊してあります。

起動した時点でA-1042が選ばれています。そのまま詳細を開くと止まります。出る例外はこれです。先頭の命令アドレスは実行のたびに変わるので省きました。
… (ucrtbased.dll) で例外がスローされました (025_call-stack-navigation.exe 内): 0xC0000005: 場所 0x0000000000000001 の読み取り中にアクセス違反が発生しました
止まるのは、顧客名を複製するだけの短い関数です。
CString CCallStackDebuggingDlg::CopyCustomerName(LPCTSTR lpszCustomerName)
{
const size_t nLength = _tcslen(lpszCustomerName);
return CString(lpszCustomerName, static_cast<int>(nLength));
}
この関数をいくら読んでも、原因は出てきません。分かるのは、渡されたポインタが読めないという事実だけです。知りたいのは、誰がこのポインタを渡したのかです。
呼び出し元へ1段ずつ戻る手順
Call Stackを開くと、自分が書いた関数が上から順に並びます。この画面では次の5段でした。
CCallStackDebuggingDlg::CopyCustomerName ← いま止まっている CCallStackDebuggingDlg::UpdateDetailPanel CCallStackDebuggingDlg::LoadOrderDetail CCallStackDebuggingDlg::OpenSelectedOrder CCallStackDebuggingDlg::OnBnClickedOpenDetail ← 画面を操作したところ

この5段の上にはucrtbased.dllのフレームが4段乗っています。下は [外部コード] に畳まれた行と、アプリの起動側です。読むのは、操作を受けた行から停止した行までの5段だけで足ります。
行をダブルクリックすると、そのフレームへ移れます。1つ下のUpdateDetailPanelが、壊れたポインタを渡している側です。
LPCTSTR lpszCustomerName = detail.lpszCustomerName;
画像のローカル ウィンドウには、停止したフレームのlpszCustomerNameが0x0000000000000001と出ています。例外メッセージが読もうとしたアドレスと同じ値です。1つ下へ移れば、この値を持ったまま渡されたdetailが見られます。
スタックに出ない関数がある理由
そのdetailを組み立てているのは、注文番号から詳細を引く検索処理です。A-1042の分岐はこうなっています。
if (sOrderNo == _T("A-1042"))
{
detail.sOrderNo = sOrderNo;
detail.lpszCustomerName = reinterpret_cast<LPCTSTR>(static_cast<ULONG_PTR>(0x1));
detail.sProductName = _T("追加開発");
detail.sAmountText = _T("¥418,000");
detail.sStatus = _T("処理中");
return TRUE;
}
顧客名のポインタに0x1が入っています。これが、止まった行まで運ばれてきた値です。
ただし、この検索処理はCall Stackに並びません。詳細を組み立て終えて、すでに呼び出しから戻っているからです。Call Stackが見せるのは、いま生きているフレームだけです。LoadOrderDetailまで戻ったら、その先はコードを読んで詰めます。
フレームを変えると変数の表示も変わる
フレームを切り替えると、変数の表示もそのフレームのものに変わります。既定ではソース、ローカル、自動変数、ウォッチ、逆アセンブルの各ウィンドウが、選んだフレームに合わせて切り替わります。
| フレーム | 見る変数 | 分かること |
|---|---|---|
| CopyCustomerName | lpszCustomerName | 読めないポインタの値 |
| UpdateDetailPanel | detail | 詳細データの中身 |
| LoadOrderDetail | sOrderNo | どの注文を開こうとしたか |
| OpenSelectedOrder | nSelectedIndex / sOrderNo | 一覧の何行目を選んだか |
止まった場所では、注文番号すら見えません。1段下へ移るとdetailが見え、その中の注文番号でA-1042だと分かります。
ここで1つ注意があります。フレームを移しても、実行位置そのものは動きません。公式ドキュメントは「実行ポインターは元のフレームに残り、黄色の矢印でマークされたままです」と書いています。続行やステップ実行は、選んだフレームではなく元のフレームから進みます。
MFCやWindowsの行を読み飛ばす判断
Call Stackには、自分が書いていない関数も並びます。MFCのDLLやWindowsのDLLの行です。そこで止まって中を読んでも、たいていは何も出てきません。自分が書いた関数の行だけを飛び石にして遡ってください。
シンボルが読み込まれていない行は淡色表示になります。右クリックの「シンボルの読み込み」で読み込めます。
ライブラリ側の行がまとめて畳まれていることもあります。上の画面もその状態で、[外部コード] の1行が代わりに出ています。ツールバーの「外部コードの表示」を切り替えると、隠れていた行が並びます。
次の1行が出たときは、その下の並びを信用しないでください。
[Frames below may be incorrect and/or missing, no symbols loaded for name.dll]
シンボルが足りず、下の並びを正しく組み立てられない状態です。自分のコードの行が消えているように見えるときは、まずこれを疑います。
まとめ
- 止まった行だけで判断しない。Ctrl + Alt + Cで呼び出し履歴を開き、一番上から呼び出し元へ順に遡る。
- 行をダブルクリックすると、変数の表示もそのフレームに変わる。
- すでに戻った関数は並ばない。そこから先はコードを読む。
- ライブラリの行は飛ばし、自分のコードの行だけを辿る。
コードを読んでも、すでに戻った関数のどこで値が書き込まれたのか絞れないときは、その変数への書き込みで実行を止める方法があります。手順は「Data Breakpoint:変数が書き換わった瞬間に止める」にあります。フレームを移った先で、ポインタの先を配列や文字列として見たいときや、エラーコードを説明文つきで見たいときは、ウォッチに書く書式指定子を「Watch Windowのフォーマット指定子」で確かめられます。
