【MFC】Call Stack (呼び出し履歴) の歩き方

【MFC】Call Stack (呼び出し履歴) の歩き方

MFCアプリがアクセス違反で止まったとき、だいたいはその行あたりに問題があり、そこを中心に見れば普通は片付きます。でも、渡された値がもう壊れていたときは、その行を何度読んでも答えが出ません。

そんなときはまずCall Stack(呼び出し履歴)を開きましょう。
停止した関数から呼び出し元へ1段ずつ遡れば、値が壊れた場所を特定できます。


目次

Call Stack (呼び出し履歴) が積み上がる仕組み

プログラムでは、関数から別の関数を呼ぶたびに、呼び出し元の情報が順に積み重なっていきます。Call Stackウィンドウを見ると、今いる関数がどこから呼ばれてきたのかを順に確認できます。

一番上の行が「現在停止している関数」であり、下に行くほど「過去(呼び出し元)」に遡ります。いま実行が置かれているフレームには矢印が付くので、どれが現在地かはひと目で分かります。

開く手順

  1. デバッグ中に停止した状態にします。ブレークポイントでもクラッシュでも構いません。
  2. メニューの「デバッグ」→「ウィンドウ」→「呼び出し履歴」を選びます。ショートカットはCtrl + Alt + Cです。

このメニューはデバッグ中しか使えません。先に止めてから開いてください。


止まった行だけでは原因が分からない例

ここからは、注文の一覧と詳細が出るだけの画面を例にします。以降の手順と画面はVisual Studio 2026 / Debug / x64 / Unicodeで確認しました。MFCはダイアログベース、共有DLLの構成です。

一覧は注文番号・顧客名・状態の3列で、A-1045からA-1042までの4件が並びます。A-1042はその1行の注文番号です。この注文の詳細データだけ、顧客名のポインタを壊してあります。

注文詳細ビューアーの起動直後の画面。注文番号・顧客名・状態の3列にA-1045からA-1042までの4件が並び、A-1042の行が選択されている。右の詳細欄はまだすべて - になっている
詳細を開く前の画面。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 ← 画面を操作したところ
アクセス違反で停止したVisual Studio。呼び出し履歴に自作関数5段が並び、ローカルにポインタの値0x0000000000000001が出ている
A-1042の詳細を開いて停止したときの呼び出し履歴とローカル

この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まで戻ったら、その先はコードを読んで詰めます。


フレームを変えると変数の表示も変わる

フレームを切り替えると、変数の表示もそのフレームのものに変わります。既定ではソース、ローカル、自動変数、ウォッチ、逆アセンブルの各ウィンドウが、選んだフレームに合わせて切り替わります。

フレーム見る変数分かること
CopyCustomerNamelpszCustomerName読めないポインタの値
UpdateDetailPaneldetail詳細データの中身
LoadOrderDetailsOrderNoどの注文を開こうとしたか
OpenSelectedOrdernSelectedIndex / 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のフォーマット指定子」で確かめられます。

目次