EternalWindows
ETW / プロバイダの実装

これまで、イベントをトレースする方法について説明してきましたが、 今回はイベントを書き込むプロバイダの実装について説明します。 アプリケーション(DLLも可能)がプロバイダになるためには、 それがETWによって認識されなければならないため、 まずはETWに対してプロバイダのGUIDを登録することになります。 この登録には、従来のプロバイダとWindows Vistaからの新しいプロバイダで方法が異なるのですが、 今回は新しいプロバイダのEventRegisterを呼び出します。

ULONG EventRegister(
  LPCGUID ProviderId,
  PENABLECALLBACK EnableCallback,
  PVOID CallbackContext,
  PREGHANDLE RegHandle
);

ProviderIdは、登録するプロバイダのGUIDを指定します。 EnableCallbackは、プロバイダが有効または無効にされた場合の通知を受け取るコールバック関数を指定します。 不要な場合はNULLで問題ありません。 CallbackContextは、EnableCallbackに渡したいユーザーデータを指定します。 EnableCallbackがNULLであるならば、CallbackContextもNULLになります。 RegHandleは、登録されたプロバイダのハンドルを受け取る変数のアドレスを指定します。

プロバイダを登録したら、EventWriteで1つのイベントを書き込むことができます。

ULONG EventWrite(
  REGHANDLE RegHandle,
  PCEVENT_DESCRIPTOR EventDescriptor,
  ULONG UserDataCount,
  PEVENT_DATA_DESCRIPTOR UserData
);

RegHandleは、EventRegisterで取得したハンドルを指定します。 EventDescriptorは、イベントを識別するEVENT_DESCRIPTOR構造体のアドレスを指定します。 UserDataCountは、UserDataの要素数を指定します。 UserDataは、イベントに含ましたいデータを格納したEVENT_DATA_DESCRIPTOR構造体の配列を指定します。

EVENT_DESCRIPTOR構造体は、1つのイベントの情報を識別します。 後で説明するように、この構造体はmc.exeによって定義されることになるため、 アプリケーションが明示的に定義することはありません。

Utypedef struct _EVENT_DESCRIPTOR {
  USHORT    Id;
  UCHAR     Version;
  UCHAR     Channel;
  UCHAR     Level;
  UCHAR     Opcode;
  USHORT    Task;
  ULONGLONG Keyword;
} EVENT_DESCRIPTOR, *PEVENT_DESCRIPTOR;
typedef const EVENT_DESCRIPTOR *PCEVENT_DESCRIPTOR;

Idは、イベントのIDを指定します。 イベントは、このIDとプロバイダのGUIDによって一意に識別されます。 Versionは、イベントのバージョンを指定します。 Channelは、イベントを分類する値を指定します。 定義されているチャンネルは、Admin、Operational、Analytic、Debugの4つであり、 AdminとOperationalの場合はイベントがイベントログに自動で書き込まれます。 Levelは、イベントのレベルを指定します。 EnableTraceExに指定したレベルがイベントのレベルより低い場合、 そのイベントはトレースの対象になりません。 レベルの意味については、TRACE_LEVEL_CRITICALなどの定数を確認してください。 Opcodeは、Taskで識別される内容の詳細を指定します。 たとえば、Taskがファイルへの入出力であれば、FILE_READやFILE_WRITEのような値を指定します。 Taskは、イベントの内容を指定します。 たとえば、そのイベントがファイルへの入出力であれば、FileIOのような値を指定します Keywordは、イベントのキーワードを指定します。 たとえば、EnableTraceExに指定したキーワードが0x02で、 イベントのキーワードが0x01である場合、両者の論理積は0になりイベントはトレースされません。

イベントに含ましたいデータは、EVENT_DATA_DESCRIPTOR構造体に格納します。

typedef struct _EVENT_DATA_DESCRIPTOR {
  ULONGLONG Ptr;
  ULONG     Size;
  ULONG     Reserved;
} EVENT_DATA_DESCRIPTOR, *PEVENT_DATA_DESCRIPTOR;

Ptrは、イベントに含ましたいデータを指定します。 Sizeは、Ptrのサイズを指定します。 Reservedは、0で問題ありません。

EVENT_DESCRIPTOR構造体とEVENT_DATA_DESCRIPTOR構造体を用意できれば、 EventWriteで1つのイベントを書き込むことができます。 次に例を示します。

int                   nValue = 5;
EVENT_DATA_DESCRIPTOR descriptor[1];

EventDataDescCreate(&descriptor[0], &nValue, sizeof(int));

EventWrite(hRegister, &eventDesc, 1, descriptor);

EVENT_DATA_DESCRIPTOR構造体の初期化には、EventDataDescCreateというマクロが使用されるのが一般的です。 このマクロは第2引数の値をPtrメンバに指定し、第3引数の値をSizeメンバに指定します。 また、Reservedメンバには0を指定します。 さて、上記の例ではdescriptorに5という値を指定していることから、 イベントには5という値が含まれることになりますが、 これだけではトレースするアプリケーションがデータの内容を理解できるようにはなりません。 仮に5という値を取得したとしても、それがどのような種類を識別するものなのかを把握できなければ意味を持ちませんし、 そもそも書き込まれたデータが文字列の場合であれば、データを数値として扱うわけにもいきません。 よって、アプリケーションがデータを理解するためには、イベントのレイアウト情報を参照することが必要不可欠ということになります。 ここで使用されるのが、インストルメンテーション マニフェストと呼ばれるファイルです。

マニフェストというと、コモンコントロールで使用されるマニフェストファイルのことのように思えますが、 あくまでその正体は、イベントのレイアウト情報を記述したxmlファイルです。 次に、インストルメンテーション マニフェストのプロトタイプを示します。

<instrumentationManifest
  xmlns="http://schemas.microsoft.com/win/2004/08/events"
  xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  >
  <instrumentation>
    <events>
      <provider>
      この中にプロバイダが書き込むことになるイベントの情報を記述
      </provider>
    </events>
  </instrumentation>

  <localization>
    <resources culture="ja-JP">
      <stringTable>
      この中に文字列の情報を記述
      </stringTable>
    </resources>
  </localization>
</instrumentationManifest>

インストルメンテーション マニフェストの最上位には、instrumentationManifest要素が存在します。 この直下にはinstrumentation要素とlocalization要素が存在し、 前者にはイベントに関する情報、後者には文字列に関する情報を記述します。 イベントはプロバイダ毎に指定することになるため、 実際にイベントを記述する箇所は、instrumentation/events/provider以下になります。 また、文字列の情報を記述する箇所は、localization/resources/stringTable以下になります。

実際にインストルメンテーション マニフェストを記述するためには、 プロバイダが書き込むことになるイベントを考えておく必要があるでしょう。 今回のイベントはウインドウの情報に関するものとし、 データにはウインドウクラスの名前、ウインドウサイズ、ウインドウの表示状態、ウインドウスタイルが含まれることになります。 次に、今回使用するインストルメンテーション マニフェストを示します。

<instrumentationManifest
  xmlns="http://schemas.microsoft.com/win/2004/08/events"
  xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  >
  <instrumentation>
    <events>
      <provider name="Sample-ETWProvider"
        guid="{A0B5676F-0DE9-424a-B817-833E605D5755}"
        symbol="g_guidProvider"
        resourceFileName="C:\project\Release\sample.exe"
        messageFileName="C:\project\Release\sample.exe"
        >

        <maps>
          <valueMap name="ShowWindow">
            <map value="1" message="$(string.Map.Show)"/>
            <map value="2" message="$(string.Map.Max)"/>
          </valueMap>
        </maps>

        <templates>
          <template tid="WindowInfo">
            <data name="classname" inType="win:UnicodeString"/>
            <struct name="rect">
              <data name="left" inType="win:UInt32"/>
              <data name="top" inType="win:UInt32"/>
              <data name="right" inType="win:UInt32"/>
              <data name="bottom" inType="win:UInt32"/>
            </struct>
            <data name="show_mode" inType="win:UInt32" map="ShowWindow"/>
          </template>
        </templates>

        <events>
          <event value="1" level="win:Informational" template="WindowInfo" symbol="g_eventDesc"/>
        </events>

      </provider>
    </events>
  </instrumentation>

  <localization>
    <resources culture="ja-JP">
      <stringTable>
        <string id="Map.Show" value="SHOW_NORMAL"/>
        <string id="Map.Max" value="SHOW_MAX"/>
      </stringTable>
    </resources>
  </localization>
</instrumentationManifest>

既に示したプロトタイプと比べて情報量は多くなっていますが、 変更されている箇所は既に述べた通りです。 変更点を1つずつ確認していきます。

<provider name="Sample-ETWProvider"
  guid="{A0B5676F-0DE9-424a-B817-833E605D5755}"
  symbol="g_guidProvider"
  resourceFileName="C:\project\Release\sample.exe"
  messageFileName="C:\project\Release\sample.exe"
>

provider要素は、プロバイダの情報を記述します。 name属性は、プロバイダの名前を指定します。 guid属性は、プロバイダのGUIDを指定します。 symbol属性は、GUIDをヘッダーファイルに定義する際の名前を指定します。 実は、作成したインストルメンテーション マニフェストは、mc.exe(メッセージコンパイラ)によってコンパイルされることになっており、 その際にはヘッダーファイルやリソースファイルなどが作成されることになっています。 resourceFileNameとmessageFileNameは、プロバイダのexeが存在するフルパスを指定します。

<events>
  <event value="1" level="win:Informational" template="WindowInfo" symbol="g_eventDesc"/>
</events>

event要素は、EVENT_DESCRIPTOR構造体の定義に使用されます。 value属性は、EVENT_DESCRIPTOR.Idに相当する値を指定します。 level属性は、EVENT_DESCRIPTOR.Levelに相当する値を指定します。 win:Informationalという定義を使用すると、4という値を指定したことになります。 template属性は、このイベントに含まれるデータを定義しているtidを指定します。 symbolは、定義されるEVENT_DESCRIPTOR構造体の名前を指定します。 最終的にヘッダーファイルに含まれる構造体は、次のようになります。

EVENT_DESCRIPTOR g_eventDesc = {
  0x1, // Id
  0x0, // Version
  0x0, // Channel
  0x4, // Level
  0x0, // Opcode
  0x0, // Task
  0x0  // Keyword
};

IdとLevelについてはevent要素に記述したため、 その値が実際に構造体に指定されています。 しかし、それ以外についてはevent要素に記述していなかったため、既定で0が指定されています。 適切な値を指定したい場合は、event要素にそのための属性を記述し、 さらにその属性のための要素を定義する必要があります。

<templates>
  <template tid="WindowInfo">
    <data name="classname" inType="win:UnicodeString"/>
      <struct name="rect">
        <data name="left" inType="win:UInt32"/>
        <data name="top" inType="win:UInt32"/>
        <data name="right" inType="win:UInt32"/>
        <data name="bottom" inType="win:UInt32"/>
      </struct>
    <data name="show_mode" inType="win:UInt32" map="ShowWindow"/>
  </template>
</templates>

template要素は、イベントに含まれるデータを定義します。 data要素の数だけデータを含むことができ、各データはname属性の名前によって識別されます。 inType属性は、データの型を指定します。 win:UnicodeStringはUNICODE文字列を意味し、win:UInt32はULONG型を意味します。 struct要素は構造体の役割を果たすことができ、複数のdata要素を持つことができます。 map属性は、数値と文字列を関連付けることができます。 ここに指定した文字列はmap要素の下で定義されている必要があります。

<maps>
  <valueMap name="ShowWindow">
    <map value="1" message="$(string.Map.Show)"/>
    <map value="2" message="$(string.Map.Max)"/>
  </valueMap>
</maps>

valueMap要素は、通常の数値を文字列に関連付ける場合に使用します。 たとえば、ウインドウが通常表示であることを表したい場合は、 1という値よりもSHOW_NORMALというような文字列のほうが分かりやすいですから、 そうした関連付けをここで行います。 message属性に指定した文字列はstring要素への参照であり、 このstring要素に関連する文字列が記述されます。

<localization>
  <resources culture="ja-JP">
    <stringTable>
      <string id="Map.Show" value="SHOW_NORMAL"/>
      <string id="Map.Max" value="SHOW_MAX"/>
    </stringTable>
  </resources>
</localization>

id属性に先のmessage属性に指定していた文字列を指定し、value属性に関連する文字列を指定します。 これにより、1という値とSHOW_NORMALの関連付けが成功したことになります。

インストルメンテーション マニフェストの作成が完了したら、これをコンシューマが参照できるようにしなければなりません。 コンシューマはTDH(Trace Data Helper) APIを使用してイベントを取得することになるわけですが、 このTDH APIがマニフェストを参照できないことには、イベントのレイアウト情報を理解することができないからです。 具体的に何をするのかというと、プロバイダのexeファイルにマニフェストの情報を格納します。 マニフェストをmc.exeを通じてコンパイルすると、リソースファイルが作成されることになるため、 これをプロジェクトに追加してビルドを行うようにするのです。 そうすれば、作成されたexeファイルにはマニフェストの情報が格納され、 TDH APIはイベントのレイアウト情報を理解できるようになります。 mc.exeを使用するには、コマンドプロント上で次の文字列を入力します。

cd C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin

まず、cdコマンドでmc.exeを存在するフォルダまで移動します。 続いて、次の文字列を入力します。

mc -h C:\project -r C:\project C:\project\sample.xml

mc.exeに対して -h <path>を指定した場合は、<path>で識別されるフォルダにヘッダーファイルが出力されます。 また、-r <path>を指定した場合は、<path>で識別されるフォルダにリソースファイルとbinファイルが出力されます。 sample.xmlがインストルメンテーション マニフェストであり、 出力されるファイルはこのマニフェストのファイル名が基になります。 当然ながら、sample.xmlはC:\project以下に存在している必要があります。 コンパイルに成功したら、出力された全てのファイルをプロジェクトのフォルダに移動し、 ヘッダーファイルとリソースファイルをプロジェクトに追加してビルドを行います。 なお、mc.exeはコンパイル時にwinmeta.xmlを必要とするため、 このファイルをmc.exeのフォルダに予めコピーしておく必要があります。 winmeta.xmlは、Includeフォルダに存在するはずです。

TDH APIがイベントのレイアウト情報を理解するためには、exeファイルにマニフェストの情報が格納されているのはもちろんのこと、 そのexeファイルがどこに存在するかを示す手掛かりも必要になります。 管理者としてwevtutil.exeを実行すれば、プロバイダの情報をレジストリに書き込むことができます。

wevtutil im C:\project\sample.xml // アンインスールの際にはimをumにする。

上記の文字列をコマンドプロント上で入力すれば、マニフェストの情報を基にレジストリへの書き込みが行われます。 参照される属性は、guid、resourceFileName、messageFileNameであり、必ず正しい値を指定しておくようにします。 ちなみに、書き込みが行われるレジストリキーは次のキー以下になります。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers

イベントの解析にはTDH APIのTdhGetEventInformationを呼び出すことになりますが、 この関数は上記キーからファイルのフルパスを参照していると思われます。

今回のプログラムは、マウスの左ボタンが押されたときにイベントを書き込みます。 書き込まれたイベントをトレースするためには、 次節のプログラムを事前に起動しておく必要があります。

#include <windows.h>
#include <evntprov.h>
#include "sample.h"

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static REGHANDLE hRegister = NULL;

	switch (uMsg) {

	case WM_CREATE:
		EventRegister(&g_GuidProvider, NULL, NULL, &hRegister);
		return 0;

	case WM_LBUTTONDOWN: {
		WCHAR                 szClassName[256];
		RECT                  rc;
		ULONG                 uMode, uResult;
		WINDOWPLACEMENT       wndpl;
		EVENT_DATA_DESCRIPTOR descriptor[3];
		
		GetClassNameW(hwnd, szClassName, 256);
		
		GetWindowRect(hwnd, &rc);
		
		wndpl.length = sizeof(WINDOWPLACEMENT);
		GetWindowPlacement(hwnd, &wndpl);

		if (wndpl.showCmd == SW_SHOWNORMAL)
			uMode = 1;
		else if (wndpl.showCmd == SW_SHOWMAXIMIZED)
			uMode = 2;
		else
			uMode = 3;
		
		EventDataDescCreate(&descriptor[0], szClassName, (lstrlenW(szClassName) + 1) * sizeof(WCHAR));
		EventDataDescCreate(&descriptor[1], &rc, sizeof(RECT));
		EventDataDescCreate(&descriptor[2], &uMode, sizeof(ULONG));

		uResult = EventWrite(hRegister, &g_eventDesc, 3, descriptor);
		if (uResult != ERROR_SUCCESS) {
			TCHAR szBuf[256];
			wsprintf(szBuf, TEXT("イベントの書き込みに失敗しました。 %x"), uResult);
			MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		}
		else
			MessageBox(NULL, TEXT("イベントを書きこみました。"), TEXT("OK"), MB_OK);

		return 0;
	}

	case WM_DESTROY:
		if (hRegister != NULL)
			EventUnregister(hRegister);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

イベントを書き込むためにはプロバイダとして登録していなければならないため、 WM_CREATEではEventRegisterを呼び出しています。 また、アプリケーションが終了する際には登録を解除するべきであるため、WM_DESTROYでEventUnregisterを呼び出しています。 WM_LBUTTONDOWNでは、イベントを書き込むためにEventWriteを呼び出しています。 イベントに含ましたいデータは、ウインドウクラスの名前、ウインドウ位置、ウインドウの表示状態であり、 それぞれGetClassName、GetWindowRect、GetWindowPlacementで取得できます。 データはEVENT_DATA_DESCRIPTOR構造体に指定することになりますが、 このときの順番はマニフェストのtemplate要素に指定した順番と同じにしておきます。


戻る