Windows Server 2022でSetUnhandledExceptionFilterがC++例外を捕捉しない問題と対処法

Windows Server 2022への移行に伴い、C++で作成したアプリケーション上でSetUnhandledExceptionFilterがワーカースレッドの未処理例外をうまく捕捉せず、予期しないアプリケーション終了が起こる事象が報告されています。本記事では、その背景や考えられる対処法を幅広く解説します。

目次

SetUnhandledExceptionFilterがC++例外をキャッチしない問題の概要

Windows Server 2016上では、_beginthread_beginthreadexで生成したワーカースレッド内でC++の未処理例外が発生しても、SetUnhandledExceptionFilterで登録したフィルタによってキャッチできるケースが一般的でした。しかし、Windows Server 2022では同じソースコードでも、未処理例外をキャッチしきれずにアプリケーションが終了してしまう事象が発生しています。

この問題は、OSバージョンが変わることで組み込まれているランタイムライブラリ(特にUCRT.dll)側の内部仕様が変わり、従来通りの例外処理が動作しなくなっている可能性が指摘されています。公式ドキュメントには明確な変更点が示されていないため、開発・運用担当者が戸惑うケースが増えているようです。

問題の背景

Windows Server 2016での挙動

Windows Server 2016では、以下のようにSetUnhandledExceptionFilterで独自の例外ハンドラを登録しておけば、ワーカースレッド上で発生したC++例外を捕捉できることが多く報告されていました。

#include <windows.h>
#include <process.h>
#include <iostream>
#include <stdexcept>

LONG WINAPI MyUnhandledExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
    std::cerr << "Unhandled exception caught by filter\n";
    // 必要に応じてログ出力や復旧処理を行う
    return EXCEPTION_EXECUTE_HANDLER;
}

void ThreadFunction(void* /*pArg*/) {
    // 何らかのエラーでC++例外が発生
    throw std::runtime_error("Test exception in worker thread");
}

int main() {
    // 独自のUnhandledExceptionFilterを登録
    SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);

    // _beginthreadでワーカースレッド生成
    _beginthread(ThreadFunction, 0, nullptr);

    // メインスレッドが終了しないよう待機
    std::cin.get();
    return 0;
}

このようにコードを実装すると、Windows Server 2016環境では、ワーカースレッドで起きたC++例外がMyUnhandledExceptionFilterによって捕捉され、アプリケーションの強制終了を防げる場合が多々ありました。捕捉後は、ログを残して例外原因を調査しつつ、必要に応じてサービスを継続するといった運用をしていた事例が報告されています。

Windows Server 2022での挙動

しかしWindows Server 2022では、上記と同じコードをコンパイル・実行しても、MyUnhandledExceptionFilterを通ることなくプロセスが終了してしまい、結果としてサービスが継続不可能になるケースが確認されています。UCRT.dll内部の例外処理フローが更新されていることや、C++例外を“未処理”として扱う場面でプロセスを即座に終了させる方向に挙動が変化した可能性が示唆されています。

実際、Microsoftが提供する公式ドキュメントでは「OSバージョンによる挙動変更」についての明確な記載は見当たりません。しかしながら、Windows Server 2022のUCRT.dllバージョンや依存するモジュールの更新が原因となり、SetUnhandledExceptionFilterで定義した独自ハンドラが呼び出されない状況が起きていると推測されています。

具体的な問題点と影響

サービスの継続性に関わる重大な問題

Windows Server上で長時間稼働させるサービスやバッチプロセスを運用している場合、想定外の例外が発生してもアプリケーションを完全に停止させず、ログ出力やリカバリ処理を行ったうえでサービスを継続させたいという要件は珍しくありません。
ところがWindows Server 2022環境に移行した後、未処理のC++例外がそのままプロセスを巻き込んで終了してしまうと、サービス停止という重大インシデントにつながります。利用者からも稼働停止の問い合わせが頻発し、ビジネス面でも大きな影響を及ぼす可能性があります。

現行コードの再利用が難しくなる

多くのシステムでは、Windows Server 2016から2022へ移行する際に、再コンパイルや大幅なソースコード修正を行わず、ほぼ同じコードベースを移植することを前提にしているケースが多いでしょう。既存のSetUnhandledExceptionFilterによるエラー処理ロジックに依存しているシステムでは、同じ動作を期待しても実際には未処理例外が補足されず、安定運用が困難になってしまいます。

可能性のある原因と内部仕様

UCRT.dllでの例外処理フローの変更

Windows 10以降、Windows Server系OSにもUniversal C Runtime(UCRT.dll)が広く導入され、C/C++ランタイム関連のAPIが一元的に提供されるようになりました。このUCRT.dllが更新される際、アプリケーションの動作に影響を与えるケースがあります。特に、未処理のC++例外をどのタイミングでプロセス終了とみなすかが変更されると、SetUnhandledExceptionFilterの出番がなくなる状況が考えられます。

スレッド作成APIと例外処理の相互作用

_beginthread_beginthreadexはCランタイムが提供するスレッド作成用APIであり、C/C++プログラムがマルチスレッドを扱う上でよく利用されます。一方、Windows API原生のCreateThreadなどを使った場合と挙動が異なる部分があります。
C++例外の未処理ハンドリングは、基本的に「ランタイムライブラリやOSが例外をどこでキャッチと判断するか」に依存しますが、この内部実装がWindows Server 2022のランタイムバージョンで大きく変更された可能性があります。

対処策・ワークアラウンドの検討

1. 公式ドキュメント・フォーラム情報の確認

現時点で、Microsoftから本件に関して公式にアナウンスがあったわけではありません。しかし、MSDN、Microsoft Q&Aフォーラム、もしくはGitHubのMicrosoft公式リポジトリなどに、同様の事象や回避策が投稿されている可能性があります。まずはこれらのコミュニティリソースやサポートチャネルで情報を探索し、問題が既知のバグとして登録されているか、改善計画やアップデートの存在を確認してみることを推奨します。

2. スレッド生成方法の切り替えを検討する

_beginthread_beginthreadexの代わりに、C++11以降で標準化されたstd::threadを利用する方法が考えられます。std::threadはC++ランタイムの管理下にありつつ、例外が発生した際の挙動が異なる場合があります。ただし、std::thread自体がnoexceptとして扱われる状況や、C++ランタイム内部で結局は同様のハンドリングが行われる可能性もあり、必ずしも解決に直結するとは限りません。
とはいえ、以下のようにstd::threadを試してみる価値はあります。

#include <thread>
#include <iostream>
#include <stdexcept>
#include <windows.h>

LONG WINAPI MyUnhandledExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
    std::cerr << "Unhandled exception in std::thread\n";
    return EXCEPTION_EXECUTE_HANDLER;
}

void threadTask() {
    throw std::runtime_error("Exception from std::thread");
}

int main() {
    SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);

    // std::threadを使った例
    std::thread worker([] {
        threadTask();
    });

    // メインスレッドが終了しないよう待機
    worker.join();
    return 0;
}

この実装でも、Windows Server 2022環境で想定通りにSetUnhandledExceptionFilterが呼ばれるとは限りませんが、_beginthread以外の手段を試すことで挙動が変化するかどうかを検証することは有益です。

3. 例外処理フローの再設計

未処理例外の捕捉をSetUnhandledExceptionFilterに依存していると、OSやランタイムのバージョン変更による影響を受けやすくなります。より堅牢なアプリケーションを目指すなら、次のような観点で例外処理フローを再設計するのも一案です。

  • スレッド関数内部での例外キャッチ
    可能であれば、スレッドのエントリーポイント付近で明示的にtry-catchを用いてC++例外を捕捉する。これにより、SetUnhandledExceptionFilterに頼らずにログ出力や復旧処理を行える。
  • 例外伝搬を活用する
    スレッド内で例外をキャッチしつつ、必要に応じてメインスレッド側に状態を通知する設計にする。たとえば、共有メモリやキュー、条件変数を用いて「スレッド内で致命的なエラーが起こりました」とメイン側にメッセージを渡し、適切な後処理をする。
  • 専用モジュールを経由する
    例外処理を一元化するために、独自に作成した“スレッド作成ラッパ”モジュール内でtry-catchとエラーハンドリングをまとめる。これにより、スレッドの起動ごとに統一された処理が適用される。

4. バージョンチェックとフォールバック

Windows Server 2016と2022の双方をサポートする必要がある場合、OSバージョンチェックを行い、条件に応じた動作変更を行う方法も考えられます。
例えば、API呼び出し前にGetVersionEx等を用いてWindows Server 2022以上であることを検知したら、_beginthreadではなくCreateThreadを利用するか、あるいはstd::threadのラッパを介するなどのフォールバックを実装するイメージです。
ただし、この方法はOSが増えるたびにコードが煩雑化し、保守性が下がる恐れがあります。将来的には根本的な解決策(たとえばMicrosoftのパッチ適用など)を検討したほうがよいでしょう。

5. Microsoftサポートへの問い合わせ

最終手段として、Microsoftの公式サポートに問い合わせをすることも有効です。企業向けサポートプログラムを利用している場合は、問題の再現手順やコードサンプルを提供することで、パッチやレジストリ設定などの具体的な回避策が提示されることがあります。
フォーラムでのQ&Aでは「改めて質問してほしい」という回答のみで具体的な解決策が得られない場合も多いですが、公式サポートであれば個別対応を期待できます。

Windows Serverバージョンごとの挙動比較(例)

以下のような表を用いて、実際に検証した結果をまとめると状況が把握しやすくなります。あくまで一例ですが、環境ごとに_beginthreadstd::threadなどの方法で未処理例外発生時の動作を比較するアプローチです。

検証環境スレッド生成方法SetUnhandledExceptionFilter呼び出し結果
Windows Server 2016 (UCRT.dll バージョンX)_beginthread呼び出される場合が多い未処理C++例外をフィルタがキャッチして継続
Windows Server 2016 (UCRT.dll バージョンX)std::thread呼び出される場合が多い同様にキャッチされるケースが多い
Windows Server 2022 (UCRT.dll バージョンY)_beginthread呼び出されないケースが報告されているプロセスが終了しサービス停止
Windows Server 2022 (UCRT.dll バージョンY)std::thread環境によってまちまちキャッチできずに終了する可能性あり

このように、環境とスレッド生成手法の組み合わせによって挙動が大きく変わるため、移行計画や本番環境導入の前に十分な検証を行うことが必須といえます。

今後の展望とまとめ

SetUnhandledExceptionFilterによる未処理例外の補足は、Windows Server 2016以前から行われてきた運用テクニックでしたが、Windows Server 2022の登場に伴い新しい課題が浮上しています。OS付属のUCRT.dll側で挙動が変わったことが原因と推測されるものの、Microsoft公式から明確な情報が示されていないため、現場レベルでの試行錯誤が続いている状況です。

とはいえ、長期的な視点ではSetUnhandledExceptionFilterに依存しすぎない、より安全な例外処理設計が望まれます。ワーカースレッドがどのような例外を投げるのかを事前に把握し、適切にtry-catchを仕込むことで想定外のアプリケーション終了を防ぐアプローチや、OSアップデートごとに検証を行う体制づくりが重要です。
特に大規模なシステムや、24時間365日連続稼働が求められるミッションクリティカルな環境では、サービスが止まるリスクを最小化するために、例外処理を徹底的に見直すことが求められます。Microsoftフォーラムの継続チェックや公式サポートへの問い合わせを行いつつ、状況に応じてワークアラウンドを柔軟に導入していくことが解決への近道となるでしょう。

コメント

コメントする

目次