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

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

クラッシュで停止したら、だいたいはその行あたりに問題があり、そこを中心に見れば普通は片付きます。でも、その場所を見ただけでは本当の原因にたどり着かないこともあります。

そんなときは Call Stack を使って、呼び出し元を順に辿っていきましょう。
原因がつかめると思います。


目次

Call Stack (呼び出し履歴) の役割

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

開き方と見方

  1. デバッグ中に停止(ブレークポイント、またはクラッシュ)した状態で、Visual Studioのメニューから 「デバッグ(Debug)」 -> 「ウィンドウ(Windows)」 -> 「呼び出し履歴(Call Stack)」 を開きます。(ショートカット: Ctrl + Alt + C)
  2. ウィンドウには、関数名がズラリと並びます。
    一番上の行が「現在停止している関数」であり、下に行くほど「過去(呼び出し元)」に遡ります。
Visual Studio の呼び出し履歴(Call Stack)ウィンドウのスクリーンショット。一番上の関数から下へ向かって呼び出し元が続いている様子

Call Stack の使い方(原因箇所の探し方)

例えば、関数が三段にネストしていて、一番奥でアクセス違反が起きたとします。

void CrashFunctionC()
{
    int* p = NULL;
    *p = 999;
}

void CrashFunctionB()
{
    CrashFunctionC();
}

void CrashFunctionA()
{
    CrashFunctionB();
}

停止した直後に見えるのは、一番上の CrashFunctionC() です。ここだけ見ても「NULLポインタを書いた」という事実しか分かりません。

原因を追うため、Call Stack ウィンドウで一段下(呼び出し元)の行をダブルクリックします。

すると、ソースコードのエディタが CrashFunctionB()CrashFunctionA() に順に戻ります。さらに重要なのは、選んだフレームの時点に合わせてローカル変数の表示も切り替わることです。

この順番で戻ると、「どこからこのクラッシュ経路に入ったか」を落ち着いて追えます。黄色い矢印の行だけを見て終わらせず、呼び出し元へ順に遡って原因箇所を特定するのが Call Stack の基本的な使い方です。


MFC/Windows API の呼び出しを見分ける

MFCやWindowsデスクトップアプリをデバッグしていると、Call Stackの途中に、グレーの文字で大量の見慣れない関数が並んでいることがあります。

> MyProject.exe!CMyView::OnLButtonDown() 行 123
  mfc140d.dll!CWnd::OnWndMsg()
  mfc140d.dll!CWnd::WindowProc()
  mfc140d.dll!AfxCallWndProc()
  user32.dll!DispatchMessageW()
  MyProject.exe!CWinThread::PumpMessage()
  ...

これは、Windows(user32.dll)がメッセージを送り、MFC フレームワーク(mfc140.dll)を経由して、最終的に OnLButtonDown が呼ばれた経路を示しています。

これらMFC本体やOS内部の処理バグであることは極めて稀ですので、基本的には 「自分が書いたソースコード(黒字で表示されていて、右端に行番号が出ている行)」 だけを飛び石のようにダブルクリックして遡り、「自分自身のコードが、どのUI操作から始まったのか」を確認するのに使います。


まとめ

  • [ ] クラッシュした行だけでは足りない。呼び出し元から不正なデータの流れを確認する。
  • [ ] Call Stack ウィンドウ(Ctrl+Alt+C)を開き、一つ下(呼び出し元)をダブルクリックして過去へ遡る。
  • [ ] 遡った先で変数の状態を確認し、不正な引数がどこで入ったかを特定する。
  • [ ] MFCやWindowsのシステム関数(グレーの行)は読み飛ばし、自分が書いたコード(黒字の行)だけを追う。
目次