Windows イベントログの取得時にメッセージが壊れる現象の回避策

背景

2019年頃の業務において、Windows XP/7 環境で使用していたプログラムを Windows 10 環境で動作検証していました。
その際、Windows イベントログから取得したメッセージが壊れる現象を確認したため、改めて以下に纏めておきます。

対象範囲

本現象は、メッセージの取得に Win32 APIFormatMessage 関数 を使用している場合に発生します。
EvtFormatMessage 関数 を使用している場合には発生しないため、対象外となります。

現象の概要

以下のように、Windows イベントログから取得したメッセージが壊れている場合があります。

Source: Edge
Event ID: 0x80000100
Message: ?????????????????档????獷????楳湯??整???慧?条????潴?????慇?条????楴湯映????楳湯?湯映??桴敲?????敬整.

現象の詳細

イベントログからメッセージを取得する流れ

  1. イベントログからイベントソース名、イベントID、イベントデータを取得
  2. イベントソース名を使用し、レジストリからメッセージファイル(DLL)のパスを取得
  3. メッセージファイル(DLL)を読み込み、ファイルハンドルを取得
  4. ファイルハンドル、イベントID、イベントデータを FormatMessage 関数に渡し、メッセージを取得

FormatMessage 関数内部の流れ

  1. イベントIDを使用し、メッセージファイル(DLL)からメッセージ定義を取得
  2. メッセージ定義にイベントデータを埋め込み、メッセージを生成


メッセージの生成

メッセージが動的に生成できるよう、メッセージ定義は引数番号および書式が指定できる文字列となっています。
例えば、メッセージ定義が "%2!s! %1!s!" で、引数が { "ABC", "DEF" } の場合、生成されるメッセージは "DEF ABC" となります。
なお、書式指定は省略でき、その場合には !s! (文字列) が適用されます。実際のメッセージ定義では、大半が省略されています。

メッセージが壊れる原因

メッセージ定義において、書式に !S! が指定されている場合に発生します。
!s! および !S! の意味は、以下の通りです。

プロジェクト設定
(API)
!s! !S!
マルチバイト文字セット
(FormatMessageA)
マルチバイト文字列
(char*)
ワイド文字列
(wchar_t*)
ワイド文字セット
(FormatMessageW)
ワイド文字列
(wchar_t*)
マルチバイト文字列
(char*)

つまり、書式に !S! が指定されている場合、プロジェクトの設定とは異なる文字セットの文字列として解釈されます。
その結果、イベントデータの文字列が正しく解釈されず、不正な文字列が挿入され、メッセージが壊れてしまいます。

Windows XP/7 においても、書式に !S! が指定されたメッセージはあるようですが、
絶対数が少なかったため目立っていなかったのではないか?と考えています。

回避策

1. メッセージ定義の !S! を !s! に置換

FormatMessage 関数は、メッセージ定義の取得や、メッセージ定義を指定したメッセージの取得ができます。
そのため、以下のように !S! を !s! に置換したメッセージ定義を使用することで、本現象を回避できます。

  1. メッセージ定義を取得
  2. メッセージ定義の !S! を !s! に置換
  3. 置換したメッセージ定義を使用してメッセージを取得

ただし、FormatMessage 関数を2回呼ぶことになるため、パフォーマンスには注意が必要となります。

C++での実装例:

// メッセージ定義を取得
FormatMessage(
    FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
    hFile,
    dwMessageId,
    0,
    (LPTSTR)&pBuffer,
    0,
    NULL);

// メッセージ定義の !S! を !s! に置換
LPTSTR pPos;
while ((pPos = _tcsstr(pBuffer, _T("!S!"))) != NULL)
{
    pPos[1] = _T('s');
}

// 置換したメッセージ定義を使用してメッセージを取得
FormatMessage(
    FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ARGUMENT_ARRAY,
    pBuffer,
    0,
    0,
    (LPTSTR)&pBuffer,
    0,
    args);

2. 書式指定に合わせたイベントデータを渡す (非推奨)

書式指定が !S! の場合、マルチバイト文字セット(FormatMessageA) では ワイド文字列(wchar_t*)、ワイド文字セット(FormatMessageW) では マルチバイト文字列(char*) のイベントデータを渡すことで、本現象を回避できます。ただし、書式指定が省略されている場合は !s! となるほか、データが文字列ではなく数値の場合もあるため、!S! が指定されている箇所を把握し、該当するデータのみ文字セットを変更する必要があります。メッセージ定義を取得して書式指定を確認するのであれば、1 の方法が効率的であり、この方法はあまり推奨されません。

3. Event Logging API から Windows Event Log API に移行する

Windows Vista 以降、Windows イベントログの APIEvent Logging API から Windows Event Log API に移行されています。
Windows Event Log API では、FormatMessage 関数ではなく EvtFormatMessage 関数を使用するため、本現象を回避できます。
変更量は多くなりますが、下位互換性を考慮する必要がなければ、これが恒久対策として最も適切な方法かもしれません。

考察

本現象は FormatMessage 関数の不具合ではなく、むしろ仕様通りに正しく動作しています。!S! が使われている理由としては、Windows XP において意図的に使用されていたものが現在まで残っている場合や、単なる文字列、あるいはワイド(Unicode)文字列を表す指定子として扱われている可能性が考えられます。これらが Windows Event Log API では正しく動作してしまうため、結果的に問題として認識されなくなっているのではないか、というのが私の考えです。