読者です 読者をやめる 読者になる 読者になる

プロバイダーを作る

Win32 ETW

さて、ようやくコードの出番です。
ミニマムなプロバイダーを作ってみましょう。

で、ソースコードなんですが、すべてここに貼りつけるわけにもいきませんので、GitHubをご覧ください。

また、適宜、以前の記事を参照しながらお読みください。

その前に

以前の記事で触れましたが、ETW には以下のような要素があります。

  • セッション
  • コントローラー
  • プロバイダー
  • コンシューマー

それぞれの役割は以前の記事を参照してください。

さて、今回作るのはプロバイダーです。
コントローラーもコンシューマーも無いのにプロバイダーを作り始めるわけです。
イベントを発行するのはいいけれど、そのイベントはどこに記録されて、どうやって見ればいいのでしょうか?

まずは、コントローラーとコンシューマーは既存のものを使います。
ETW に関するツールWindows に標準でいくつか備わっていますので、プロバイダーだけでも、その動作を観察することができるのです。

今回使用するツールは、イベント ビューアーです。
もっとも馴染み深いですし、GUI なのでわかりやすいですね。

ProviderManiest1.man

では、ソースコードを順に解説していきます。

まず重要なのはマニフェストです。
provider 要素より前は定型句なので、気にする必要はありません。

provider 要素

<provider
  guid="{3E7764B2-10E3-4396-87D7-0B3A81038806}"
  name="SampleProvider1"
  symbol="PROVIDERID_SampleProvider1"
  resourceFileName="Provider1.exe"
  messageFileName="Provider1.exe"
  parameterFileName="Provider1.exe">

guid はプロバイダーの識別子です。guidgen.exe を使用してユニークなものを生成してください。

name は適当。

symbol は、C++ ソースコードで使用する名前です。

resourceFileName、messageFileName、parameterFileName の 3 つは…まぁ、詳しいことは省略します(よくわかってない)。
これらのパラメーターはリソースを含むファイルのフルパスを記述するということになっているのですが、そんなもの、コーディングの時点ではわかりません。
幸い、これらのパラメーターは後でインストール時に変更可能ですので、ここでは仮にファイル名だけ入れておくことにします。

channel 要素

<channels>
  <channel chid="1" name="Test" type="Admin" enabled="true"/>
</channels>

チャンネルの定義です。

chid はチャンネルの識別子です。数値に限らず、文字列でも構いません。

name は名前です。

type は前回説明した 4 つの中から選びますが、基本は Admin です。

enabled は、このチャンネルが規定で有効であることを示します。

template 要素

<templates>
  <template tid="t1"></template>
</templates>

今は詳しくは説明しません。このサンプルでは内容のない空のテンプレートを定義していますが、とりあえず必要なんだと思っておいてください。

event 要素

<events>
  <event value="1" channel="1" level="win:Informational" message="$(string.Event.Hello)" symbol="EVENTDESC_Hello"/>
</events>

value はイベントの識別子です。イベント ビューアーではイベント ID として表示されます。

channel は上で定義した channe 要素の chid です。

level はあらかじめ定義済みのものを使用しています。
前回も書きましたが、win: プレフィックスが付くものは winmeta.xml ファイルで定義されています。

message は人間が読めるメッセージです。channel が Admin の場合は必須です。
この $(string.なんちゃら) という書き方は、後で説明する stringTable を参照していることを表します。

symbol は provider と同様 C++ ソースコードで参照する名前です。

resources 要素 / stringTable 要素

<resources culture="ja-JP" xml:lang="ja-JP">
  <stringTable>
    <string id="Event.Hello" value="はろー"/>
  </stringTable>
</resources>

resources 要素は多言語対応に使用する要素ですが、今回のサンプルでは日本語のメッセージしか定義していません。

string 要素の id は event 要素から参照するのに使用しています。event 要素の message 属性を見比べてみてください。

value はメッセージそのものです。

マニフェストコンパイル

マニフェストは書いただけではダメで、コンパイルする必要があります。

Visual Studio でプロジェクトを開いて、ソリューション エクスプローラーから ProviderManifest.man のプロパティを開いてみてください。
カスタムビルドで、以下のようなコマンドラインが指定されています。

mc.exe -n -u -c "%(FullPath)"

まぁ、オプションに大した意味はありません。
mc.exe (メッセージ コンパイラー)の詳細は MSDN を参照してください。

コンパイルすると、以下のようなファイルが出力されます。

ProviderManifest1.h
マニフェストの内容を C++ 形式で表したものです。
ProviderManiest1.rc
*.bin ファイルをリソースとしてプログラムに取り込むためのリソース スクリプトです。
ProviderManifest1TEMP.bin
マニフェストの内容をバイナリ形式で表したもの…だと思います。
TEMP は Temporary ではなくて Template の略らしいです。
MSG00001.BIN
MSG00002.BIN
言語リソースです。言語の数だけファイルができます。
今回は日本語リソースしか定義していませんが、2 つあるのは、既定で英語リソースが含まれるためでしょうか…?

Provider1.cpp

ソースコードはシンプルなもんです。
マニフェストコンパイルして出来た ProviderManifest1.h をインクルードしている点に注意してください。

REGHANDLE hTrace = NULL;
ULONG result = EventRegister(&PROVIDERID_SampleProvider1, NULL, NULL, &hTrace);
if (result != ERROR_SUCCESS)
{
	return 1;
}

result = EventWrite(hTrace, &EVENTDESC_Hello, 0, NULL);

result = EventUnregister(hTrace);

まず EventRegister 関数でプロバイダーをシステムに登録します。
イベントを発行するのは EventWrite 関数です。
最後に EventUnregister 関数でプロバイダーの登録を解除します。

今回、ミニマム サンプルなので、イベントには付随データが一切ありません。
これだと、「何かが起きた」ということはわかりますが、「何が起きたか」を知るには実用的とは言えません。
イベントには様々な付随データを記録することができ、その場合、EventWrite の前に付随データの組み立てのためのコードが入ります。
ただし、そこが複雑になったとしても、基本的な処理の流れは、この 3 ステップだけです。

リソース ファイル

EXE には、マニフェストコンパイルして出来た *.bin ファイルをリソースとして取り込まなければなりません。
そのために、Provider1.rc というファイルを作り(とりあえずバージョン リソースだけを含めています)、そこにインクルードするという形で、ProviderManifest1.rc を含めています。
リソース ファイルをインクルードするには、Visual Studio のリソース ビューで「リソース ファイルのインクルード」を使用します。

f:id:aetos382:20141006021035p:plain

プロバイダーの登録

ここまで来ればプロバイダーはビルドすることができるのですが、まだ動かすことができません。
ビルドしたプロバイダーをシステムに登録しなければならないのです。

そのために RegiterProvider1.cmd というファイルを用意しています。
ビルドすると出力フォルダーにコピーされますので、開いてみてください。

ちゃんとした製品プログラムでは、これはインストーラーでやるべきことでしょう。

set manifest=%~dp0ProviderManifest1.man
set resource=%~dp0Provider1.exe

icacls "%resource%" /grant "NT AUTHORITY\Local Service":RX /Q
wevtutil.exe im "%manifest%" /rf:"%resource%" /mf:"%resource%" /pf:"%resource%"

プロバイダーをシステムに登録するには、wevtutil というツールを使います。Windows に標準で入っているツールです。
/rf、/pf、/mf スイッチで、マニフェストの provider 要素の属性を上書きしています。

icacls を使用しているのは、このファイルをシステムが読み取るのに Local Service アカウント権限で読み取るためです。
あらかじめ権限を付与しておかないと、wevtutil が警告を出します。

RegisterProvider1.cmd は管理者権限で実行する必要があります。
UnregisterProvider1.cmd はプロバイダーの登録を解除するスクリプトです。

なお、プロバイダーが登録されている状態では、exe がシステムによってロードされていますので、コードを変更してビルドするとエラーが発生します。
ビルドする際は面倒ですが一度 UnregisterProvider1.cmd を使って登録を解除する必要があります。

イベント ビューアーで見る

プロバイダーの登録を済ませれば、イベント ビューアーの左側のツリーにチャンネルが表示されているはずです。
Provider1.exe を実行すれば、真ん中のペインにイベントが表示されます。

補足と注意

今回、発行されたイベントを見るのにイベント ビューアーを使いました。

しかし、基本的にイベント ビューアーはイベント ログのためのツールです。
このシリーズの最初の記事に書きましたが、イベント ログは ETW の上に構築されているシステムであって、ETW そのものではありません。

今回、その差異が表れているのがマニフェストです。
実は、純粋な ETW としては、チャンネルも空のテンプレートも必要ありません。それらがなくてもイベントは発行できますし、(イベント ビューアー以外のツールでは)読み込めます。
ただし、イベント ビューアーで見るためには、どうもこれらを書かないといけないようなのです。

ちなみに。
これが弱点となるのが、.NET FrameworkEventSource クラスです。
実は EventSource は、C# 等のマネージ コードでマニフェストを書くための仕組みです。
そのために、task や opcode 等をコードで書けるようになっています。
が、何故か EventSource は channel を定義することができず、そのため、EventSource で発行したイベントはイベント ビューアーで見ることができません(繰り返しますが、他のツールでは見ることができます)。

.NET 向けの ETW ライブラリは他にもいろいろあるようで、中には channel をサポートしたものものあります。
そうしたものも、この連載の中で追い追い採り上げて行きたいと思います。