Release ビルドだけで起きるバグの調べ方:DebugView / PDB / 最適化の切り分け

【MFC】Releaseビルドのデバッグ完全ガイド:OutputDebugString・DebugView・PDB設定の実践手順

Releaseビルドでだけ再現するバグは、Debug構成に戻しても原因に近づけません。ログを仕込もうにもTRACEはReleaseでは消え、デバッガをアタッチしてもウォッチの値が最適化で見えないことがあります。

この記事では、Releaseビルドのまま原因を追う3つの手段――OutputDebugString + DebugViewによるログ取得、/Zi + /DEBUGによるPDB出力、/Odや #pragma optimizeによる部分的な最適化無効化――を、設定手順・コード例・トラブルシュートまで網羅します。

ログを出すコードだけを確認したい場合は「OutputDebugStringで自作ログ出力」、デバッガのないPCで受信・保存する手順だけが必要なら「DebugViewでリリース後のログを見る」を先に参照してください。

未初期化変数、ASSERTの中の副作用、同期のない共有変数のように、Releaseビルドだけで落ちる原因の側から確かめたい場合は「【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順」が先です。ここでは、原因の見当がまだつかないときに、ログ・PDB・最適化の切り替えで落ちる場所を絞り込みます。

目次

状況ごとの判断フロー

ただログを足すだけで分かる不具合もあれば、PDBやダンプがないと追えない不具合もあります。やりたいことに合わせて、次の表の手段を上から順に試してください。

やりたいこと使う手段準備注意点
Releaseでも実行中のログを見たいOutputDebugString + DebugViewDebugViewをインストール、Capture Win32をONTRACEはReleaseで消えるため使えない
クラッシュ位置を行番号で追いたい/Zi + /DEBUGでPDBを出力プロジェクトプロパティ2箇所を変更PDBをexeと同じビルドで保管する
変数の値がウォッチで表示されない/Odまたは #pragma optimize("", off)対象関数だけに限定する調査後は必ず元に戻す
Releaseだけ挙動が変わる原因を切り分けたい未初期化変数・ASSERT内副作用・タイミング依存を確認ログに入力値と分岐結果を残すDebugで再現しない前提で切り分ける
ユーザー環境のクラッシュを後から追いたいPDB + ダンプファイル解析シンボルサーバまたはローカルPDB保管ビルド番号とPDBの対応を管理する

この記事で使う開発環境

OSWindows 11
IDEVisual Studio 2026
ワークロードC++ によるデスクトップ開発
MFCMFC / ATLサポートを有効
構成x64 / Release
確認ツールDebugView v4.90 / v5.0 (Sysinternals。掲載画像はv4.90)

手段1:OutputDebugString + DebugViewでログを取る

ついTRACEでログを足したくなりますが、MFCのTRACEマクロはDebug構成でしか動きません。Release構成ではTRACEが空に展開されるため、何も出力されません。Releaseビルドでログを残すには、Windows APIのOutputDebugStringを使います。

TRACEとOutputDebugStringの違い

項目TRACE (MFCマクロ)OutputDebugString (Win32 API)
Releaseで動くか動かない(空に展開される)動く
出力先Visual Studio出力ウィンドウデバッガ出力ウィンドウ / DebugView
デバッガなしで見れるか見れないDebugViewで見れる
パフォーマンス影響Releaseでは無し高頻度出力では処理コストに注意
書式付き出力TRACE(_T("x=%d"), x)自前でフォーマットが必要

基本的な使い方

// Release でも DebugView で確認できるログ出力
CString msg;
msg.Format(_T("[MyApp] value=%d, state=%d\n"), value, state);
::OutputDebugString(msg);

プレフィックス(例: [MyApp])を付けておくと、DebugViewのフィルタ機能で自分のログだけを抽出できます。システム全体のデバッグ出力が混ざる環境では必須です。

書式付きヘルパー関数を用意する

毎回CString::Format + OutputDebugStringを書くのは冗長です。TRACEと同じ感覚で使えるヘルパーを作ります。

// ReleaseTrace.h — Release でも使える書式付きログ出力
#pragma once

static void ReleaseTrace(LPCTSTR format, ...)
{
    va_list args;
    va_start(args, format);

    CString message;
    message.FormatV(format, args);
    ::OutputDebugString(message);

    va_end(args);
}

呼び出し側はTRACEと同じ書き方です。

ReleaseTrace(_T("[MyApp] OnTimer: id=%d, elapsed=%dms\n"), nIDEvent, elapsed);
ReleaseTrace(_T("[MyApp] CreateFile failed: path=%s\n"), filePath);

条件付きで出力を切り替える

調査が終わったらOutputDebugStringの呼び出しを残したくない場合は、プリプロセッサで制御します。

// RELEASE_TRACE_ENABLED を定義したときだけ有効にする
#ifdef RELEASE_TRACE_ENABLED
  #define RTRACE(fmt, ...) ReleaseTrace(fmt, __VA_ARGS__)
#else
  #define RTRACE(fmt, ...) ((void)0)
#endif

プロジェクトプロパティのC/C++ → プリプロセッサ → プリプロセッサの定義 にRELEASE_TRACE_ENABLEDを追加すれば有効になります。出荷時は定義を外すだけで全出力が消えます。

DebugViewの起動と設定

DebugViewはSysinternalsの無料ツールです。OutputDebugStringの出力をリアルタイムで表示します。

  1. Microsoft LearnのSysinternalsページからDebugViewをダウンロードします
  2. Dbgview.exeを起動します
  3. メニューCapture → Capture Win32にチェックを入れます(Ctrl + W)
  4. この状態でReleaseビルドのexeを実行します
  5. OutputDebugStringの出力がリアルタイムに表示されます

Visual Studioのデバッガをアタッチしている場合は、OutputDebugStringの出力はVisual Studio側が受け取ります。DebugViewで見たい場合は、デバッガなしで実行してください。

DebugViewの画面。[ReleaseTrace] で始まるログが複数行表示されている。Capture Win32が有効の状態

DebugViewのフィルタ設定

システム全体のデバッグ出力が大量に流れる環境では、フィルタを設定します。

  1. メニューEdit → Filter/Highlightを開きます(Ctrl + L)
  2. Includeに自分のプレフィックスを入力します(例: [MyApp])
  3. OKを押すと、プレフィックスを含む行だけが表示されます

Highlight欄に色付けルールを追加すると、エラーログだけ赤で表示するような使い方もできます。

DebugViewのFilter/Highlightダイアログ。Include欄に [ReleaseTrace] と入力し、自分のログだけを抽出している状態

DebugViewにログが出ないときの確認項目

やみくもにログを増やす前に、受け取る側の設定を確認します。OutputDebugStringを入れたのにDebugViewに何も出ないときは、次の表の原因を上から順に確かめます。

原因確認方法対処
Capture Win32がOFFDebugViewのメニューCaptureを確認Ctrl + WでONにする
Visual Studioデバッガがアタッチ済みVSの出力ウィンドウにログが出ていないか確認デバッガなしで実行(Ctrl + F5)するか、VSを閉じる
権限の不一致アプリが管理者権限で動いている必要に応じてDebugViewを昇格する
フィルタで除外されているFilter/HighlightのInclude / Excludeを確認一度フィルタをクリアして確認する

最も多いのは「Visual Studioからデバッグ実行しているため、デバッガがOutputDebugStringを横取りしている」パターンです。DebugViewで確認するときは、デバッガなしで実行してください。


手段2:/Ziと /DEBUGでPDBを出力し、行番号を追う

ログだけで原因を絞れない場合は、Release構成でもPDB(プログラムデータベース)を出力します。PDBがあれば、クラッシュダンプやデバッガ上でシンボル名、関数名、行番号を追えます。

設定手順

  1. プロジェクトのプロパティを開きます
  2. 上部の構成をRelease、プラットフォームをx64にします
  3. C/C++ → 全般 → デバッグ情報の形式 を プログラム データベース (/Zi) にします
  4. リンカー → デバッグ → デバッグ情報の生成 を はい (/DEBUG) にします
  5. リンカー → 最適化 → 参照 を /OPT:REFに明示します
  6. デバッガでの追いやすさを優先する間は、リンカー → 最適化 → COMDATの圧縮 を /OPT:NOICFにします。出荷版と同じ折りたたみ条件で再現確認するときだけ /OPT:ICFに戻します
Visual Studioのプロジェクトプロパティ。Release | x64構成で /Ziと /DEBUGが設定されている画面

/Ziと /DEBUGの関係

設定役割確認点
/Zi(C/C++ コンパイラ)関数・変数・型・行位置などのシンボル情報をコンパイラPDBへ出力する生成した .objは対応するPDBの名前と署名を参照する
/DEBUG(リンカー)実行ファイルに対応するリンカーPDBを生成するソース行を追うにはコンパイラ側のデバッグ情報も必要

現在のMSVCでは /Ziが /DEBUGを暗黙に有効にしますが、プロジェクト設定を監査しやすくするため、ここではコンパイラとリンカーの両方を明示して確認します。

/DEBUGを付けたときのリンカー最適化

/DEBUGを指定すると、リンカーの既定動作が変わります。未参照関数やデータの除去(/OPT:REF)と同一COMDATの折りたたみ(/OPT:ICF)が無効になり、Releaseビルドの実行ファイルサイズが増えます。

Releaseの未参照コード除去を維持する場合は /OPT:REFを明示します。一方、/OPT:ICFは同一内容の関数を同じアドレスへまとめるため、スタックトレース、ブレークポイント、ステップ実行が分かりにくくなることがあります。調査開始時は /OPT:NOICFを使い、出荷版と同じ条件での再現が必要な段階だけ /OPT:ICFに切り替えます。

// .vcxproj 内の該当箇所(参考)
<Link>
  <GenerateDebugInformation>true</GenerateDebugInformation>
  <OptimizeReferences>true</OptimizeReferences>
  <EnableCOMDATFolding>false</EnableCOMDATFolding>
</Link>

PDBの管理と保管

PDBは実行ファイルと1対1で対応します。異なるビルドのPDBを当てると、行番号がずれます。

  • 開発中は、ビルド出力フォルダにPDBが自動生成されるため、そのまま使えます
  • 社内配布では、リリースごとにPDBをバージョン付きフォルダに保管します
  • ユーザー環境でクラッシュしたときは、ダンプファイル(.dmp)を回収し、対応するPDBを使ってVisual Studioで解析します

PDBをユーザーに配布する必要はありません。PDBはソースコードのパスや内部構造を含むため、外部公開しない場所に保管してください。


手段3:最適化を部分的に切って変数を追う

Release構成では、最適化によって変数がレジスタに格納されたり、関数がインライン展開されたりします。デバッガのウォッチで 「最適化により使用不可」 と表示される場合、一時的に最適化を切って調査します。

方法A:#pragma optimizeで関数単位に切る

プロジェクト全体の最適化を切ると、Releaseの再現条件が変わることがあります。最初は疑わしい関数だけに限定します。

#pragma optimize("", off)
void CMyDialog::OnProcessData()
{
    // この関数だけ最適化を抑えて調査する
    int result = CalculateValue(m_input);
    // ここで result の値をウォッチで確認できる
    if (result < 0) {
        // Release ではここに来るのに Debug では来ない、を追跡する
        ReleaseTrace(_T("[MyApp] result=%d (unexpected)\n"), result);
    }
}
#pragma optimize("", on)

方法B:プロジェクト全体を /Odにする

関数単位で絞り込めない場合は、プロジェクト全体の最適化を切ります。

  1. C/C++ → 最適化 → 最適化 を 無効 (/Od) に変更します
  2. ビルドして現象を再現します
  3. /Odで再現しなくなった場合は、最適化依存のバグです
  4. /Odでも再現する場合は、未初期化変数や論理エラーの可能性が高くなります

方法C:特定ファイルだけ最適化を変える

ソリューションエクスプローラーで特定の .cppファイルを右クリックし、プロパティを開きます。そのファイルだけ /Odに変更できます。プロジェクト全体の設定は変わりません。

重要: /Odや #pragma optimize("", off) は調査用です。原因が分かったら必ず元に戻してください。調査設定のまま出荷すると、パフォーマンスが落ちるだけでなく、最適化依存の問題が隠れたまま残ります。


Releaseだけで起きるバグの典型パターン

Releaseビルド固有のバグは、パターンごとに追い方が決まっています。闇雲にログを入れる前に、どのパターンに該当するかを判断します。

パターンDebugではなぜ見えないかReleaseでの症状追い方
未初期化変数Debugランタイムがメモリを0xCC等で埋めるため、偶然動くことがある不定値で分岐が変わる、アクセス違反初期化漏れをコード検索、ログで入力値と分岐を残す
ASSERT内の副作用DebugでASSERTの中身が評価されるReleaseでASSERTごと消え、副作用も消えるASSERT内の関数呼び出し・代入を外に出す
タイミング依存(競合)Debugは最適化なしで実行が遅く、競合が表面化しにくい最適化で実行時間が変わり、競合や待ち合わせ漏れが出るスレッドID、時刻、状態遷移をOutputDebugStringで残す
最適化による再配置Debugはコード順に実行されるコンパイラが処理順を変え、volatileでない変数の読み取りが期待と異なる#pragma optimizeで範囲を絞り、volatileを検討する
Release専用のマクロ差異Debugで有効なマクロ(_DEBUG, ASSERT)がReleaseで消えるDebug専用パスに依存した処理が丸ごと消失プリプロセッサ展開結果を確認する

原因そのものの切り分けは「【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順」で詳しく扱っています。


実践:Releaseビルドのバグ調査フロー

実際の調査手順を時系列で並べます。

ステップ1:現象を確認する

  • Releaseビルドで現象が確実に再現するか確認します
  • Debugビルドで同じ操作をして再現しないことを確認します
  • 再現条件(操作手順、データ、タイミング)を記録します

ステップ2:ログを仕込む

  • 疑わしい箇所の入口と出口にOutputDebugStringを入れます
  • 変数の値、分岐の結果、関数の戻り値をログに含めます
  • DebugViewで出力を確認しながら範囲を狭めます
void CMyDialog::OnProcess()
{
    ReleaseTrace(_T("[MyApp] OnProcess: start, m_count=%d\n"), m_count);

    int result = DoCalculation(m_data, m_count);
    ReleaseTrace(_T("[MyApp] OnProcess: DoCalculation returned %d\n"), result);

    if (result == 0) {
        ReleaseTrace(_T("[MyApp] OnProcess: unexpected zero result\n"));
        return;  // ← Release だけここに来る?
    }

    int divided = 100 / result;
    ReleaseTrace(_T("[MyApp] OnProcess: divided=%d\n"), divided);
}

ここまでのコードをReleaseビルドで実行し、DebugViewを開いた状態で問題の操作を再現すると、[MyApp] プレフィックス付きのログがリアルタイムで流れます。

Releaseビルドで実行したアプリのログ出力例。OutputDebugStringの呼び出し結果を確認できる

ステップ3:PDBでクラッシュ位置を特定する

  • ログで範囲を絞れない場合は、/Zi + /DEBUGを設定してビルドします
  • Visual StudioからRelease構成でデバッグ実行(F5)します
  • クラッシュ時に行番号とコールスタックを確認します

ステップ4:最適化を切って変数を確認する

  • 変数がウォッチで見えない場合は、その関数だけ #pragma optimize("", off) を付けます
  • /Odで現象が消える場合は、最適化依存のバグと判断します
  • 原因が分かったら、#pragma optimizeと /Odを戻します

OutputDebugStringのパフォーマンスと出荷時の扱い

OutputDebugStringは呼び出しごとにカーネルオブジェクトを経由するため、高頻度ループ内で使うと性能に影響します。

状況影響度対応
ボタン押下時に数行出す無視できる出荷版でも残しておいてよい
描画ループ内で毎フレーム出す目に見える遅延条件付き(Nフレームに1回)か、出荷時は除去
ファイルI/Oループ内で毎行出すI/O性能に直接影響調査終了後に除去、またはプリプロセッサで制御

出荷時に全てのOutputDebugStringを外したい場合は、前述のRTRACEマクロのようにプリプロセッサで制御するか、呼び出し側で #ifdefを使います。デバッガがアタッチされていない状態でのOutputDebugStringは、受け手がいなくてもわずかなオーバーヘッドがあります。


Releaseデバッグ設定の一覧(コピペ用)

Release構成で調査用に変更する設定を一覧にまとめます。調査終了後にどこを戻すか確認するためのチェックリストとしても使えます。

プロパティパス通常のRelease値調査時の設定戻し忘れリスク
C/C++ → 全般 → デバッグ情報の形式プロジェクトによる/Zi低(PDBが増える)
リンカー → デバッグ → デバッグ情報の生成プロジェクトによるはい (/DEBUG)リンカー最適化の既定値変更に注意
リンカー → 最適化 → 参照はい (/OPT:REF)/OPT:REF(明示)なし
リンカー → 最適化 → COMDATの圧縮はい (/OPT:ICF)/OPT:NOICF(調査時)出荷条件で再現するときは戻す
C/C++ → 最適化 → 最適化/O2/Od高(出荷前に必ず /O2に戻す)

/Ziと /DEBUGは、Releaseの出荷版でも常時有効にしている現場があります。/Zi自体はコンパイラ最適化へ影響しません。PDBを保管するとユーザー環境のクラッシュダンプを解析できますが、/DEBUG指定時はリンカー最適化の既定値も確認してください。


関連記事


まとめ

  • Releaseでログを取るにはOutputDebugStringとDebugViewを使います。TRACEはReleaseで空になるため使えません
  • DebugViewにログが出ない場合は、Capture Win32のON/OFF、デバッガのアタッチ状態、管理者権限の差を確認します
  • クラッシュ位置を行番号で追うには /Zi + /DEBUGでPDBを出力します。調査開始時は /OPT:REFと /OPT:NOICFを使い、必要な場合だけ出荷版と同じ /OPT:ICFへ戻します
  • 変数がウォッチで見えないときは #pragma optimize("", off) で関数単位に最適化を切ります。プロジェクト全体の /Odは最終手段です
  • /Odや #pragma optimizeは調査用です。原因が分かったら必ず元に戻します
  • /Ziと /DEBUGは出荷版でも有効にできます。対応するPDBを保管し、リンカー最適化の設定も明示して管理します
目次