Posts filed under 'OS の内部動作'

OS の起動時間短縮(2/2)~ビルトインのデバイスドライバを遅延ロード

■はじめに
昨年9月のエントリで、「今回と次回は、WEC の起動時間を短縮する方策を、二つ紹介します。」と述べて一つめの方策を書いた後、一年近く時間が空いてしまいましたが、二つめの方策を書きます。

前回紹介した方策では、RAM レジストリを永続化した構成において、RAM レジストリの初期化時間を短縮する、というものでした。今回紹介するのは、ビルトインのデバイスドライバのうち、起動直後ないし起動中に動作していることが必須ではないものを、OS 起動中にはロードされないようにする(必要になった時点で後からロードする、つまり遅延ロードする)、という方策です。

この方策により、WEC が起動して初期画面(標準シェルを組み込んだ場合は、デスクトップ画面)が表示されるまでの時間を若干短縮することが可能です。短縮される時間は、遅延ロードするドライバ群の初期化処理に要する時間の合計です。従って、初期化処理に要する時間が長いドライバがあれば、遅延ロードさせる対象の候補となります。

注意:WEC 2013 には、標準シェル(explorer.exe)は付属しません。代わりに、最少機能のミニシェルを使う必要があります。

もちろん、遅延ロードさせるドライバは、WEC の起動直後ないし起動中に動作していなくても構わないものに限られます。たとえば、ディスプレイドライバは、WEC のウィンドウシステムである GWES が必要としますので、遅延ロードさせるわけにはいきません。また、LCD ディスプレイを接続した端末であれば、LCD バックライトのドライバも、WEC の起動中に動作を開始しなければいけません(そうでないと、LCD の画面が真っ暗で、初期画面が表示されませんからね)。逆に、USB や SD のホストコントローラのドライバは、遅延ロード対象の候補になるでしょう。

USB や SD の、対向デバイスごとのドライバ、つまりクラスドライバは、対向デバイスが接続された時点で初めてロードされる Plug & Play ドライバですが、それらをロードするホストドライバを遅延ロードさせても、問題ないケースは多いでしょう。他の候補としては、バッテリードライバも挙げられます。これら、ビルトインのデバイスドライバ(※USB や SD のクラスドライバのような Plug & Play ドライバではなく、OS の起動時にロードされるドライバ)のうち、遅延ロード可能なものを洗い出して遅延ロードさせることにより、WEC が起動して初期画面が表示されるまでの時間を、数10[ms]~数100[ms]、場合によっては数秒程度短縮できるかも知れません。

ビルトインのデバイスドライバを遅延ロードさせることによる、WEC の初期画面が表示されるまでの時間の短縮は、OS のコンフィグレーションや、遅延ロードさせるドライバが初期化時に行う処理内容、及びドライバが制御するハードウェアに依存します。つまり、どれくらいの時間が短縮されるかは、ケースバイケースです。そのため、ここでは具体値には触れず、上記の定性的な説明に留めます。

なお、今回の説明に通じる内容は、WEC 2013 のディベロッパーガイドでも触れられています。興味のある方は、MSDN にある、次のページもご覧になってみて下さい。

 Optimize Driver Load Time (Compact 2013)
 https://msdn.microsoft.com/en-us/library/dn194541.aspx

■ビルトインのデバイスドライバ
さて、遅延ロードの手順について説明する前に、ビルトインのデバイスドライバとは何を指すのかを、明らかにしておきましょう。上で、ビルトインのデバイスドライバを「OS の起動時にロードされるドライバ」と書きましたが、OS の起動時にロードされるかどうかは、レジストリ設定で決まります。具体的には、ドライバのレジストリキーが
 [HKEY_LOCAL_MACHINE\Drivers\BuiltIn]
の下に設定されていると、OS の起動時にロードされるのです。このことは、WEC 2013 のディベロッパーガイドの次のページで説明されています:

 Device Manager (Compact 2013)
 https://msdn.microsoft.com/en-us/library/jj659831.aspx

上のページの中ほどにある、”Registry Settings” という項をご覧ください。ここに、デバイスドライバのレジストリ項目の説明もあります。

■デバイスドライバのロード防止設定
上述したように、レジストリキーを [HKEY_LOCAL_MACHINE\Drivers\BuiltIn] の下に配置したデバイスドライバは、OS の起動時に自動的にロードされます。この設定を変え、”BuiltIn” ではなく、他のキーの下にレジストリキーを配置すると、OS の起動時にはロードされません。そのような場合は、ActivateDevice[Ex]() を使って明示的にロードしない限り、ロードされることはありません。

たとえば、バッテリドライバのレジストリキーは
 [HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Battery]
ですが、これを変更し、
 [HKEY_LOCAL_MACHINE\Drivers\AddOn\Battery]
にすると、起動時にロードされなくなります。つまり、ビルトインのデバイスドライバを OS の起動時に自動ロードされないようにするいは、そのレジストリキーを変更し、[HKEY_LOCAL_MACHINE\Drivers\BuiltIn] の下ではない階層に移せばよいのです。

ただし、バッテリドライバのように、WEC 標準のデバイスドライバのレジストリキーを変更してしまうと、不都合を生む場合があります。そのドライバに対するレジストリ設定が、複数の .reg ファイルに分割して記述されている場合です。実際、端末が違っても変わらない共通的な設定は WEC 付属の共通設定を記述した .reg ファイル(%_WINCEROOT%/public/common/oak/files/common.reg)に記述し、端末ごとに変わる設定を、OS Design プロジェクトの .reg ファイルや、あるいは、BSP の .reg ファイルに記述する、ということは珍しくありません。その際、WEC 標準で決まっているレジストリキーを変更してしまうと、不整合が起きてしまう危険があります(※当該ドライバに対する、全ての .reg ファイルに記載された設定を変更できず、一部は BuiltIn 配下のままになってしまう、というケースが考えられます)。その危険を回避するには、レジストリキーは標準設定のまま(※つまり、ビルトインのデバイスドライバのレジストリキーは、 BuiltIn の下に配置する設定のまま)で、自動ロードされるかどうかだけを変更することが必要です。そして、そのためのレジストリ項目があるのです。

デバイスドライバの自動ロードは、Flags というレジストリ項目の設定値で防止できます。上で紹介したページの、”Registry Settings” という項にある表を見て下さい。表の4行目にある、DEVFLAGS_NOLOAD というフラグが、そうです。デバイスドライバのレジストリ設定で、Flags の値が DEVFLAGS_NOLOAD (0×00000004) を含むと、そのドライバはロードされません。つまり、BuiltIn の下にレジストリキーが配置されていても、OS の起動時に自動ロードされません。ただし、OS の起動時に限らず、ActivateDevice[Ex]() を使ってもロードできません。

■ビルトインのデバイスドライバの遅延ロード手順
デバイスドライバのレジストリ設定において、Flags に DEVFLAGS_NOLOAD (0×00000004) を含む値を設定することにより、ビルトインのデバイスドライバであっても、OS の起動時に自動的にロードされるのを防ぐことができるのは分かりました。しかし、そのままでは、ActivateDevice[Ex]() を使ってもロードできず、ロードできないままになってしまいます。では、どうすればよいのでしょうか?

解決方法は、簡単です。ドライバをロードする時に、レジストリ設定を一時的に変更すればよいのです。つまり、ActivateDevice[Ex]() を呼び出す前に、レジストリの Flags の値から DEVFLAGS_NOLOAD フラグを取り除き、ActivateDevice[Ex]() の呼び出しが終わったら、再度 DEVFLAGS_NOLOAD フラグを加えればよいのです。

つまり、ビルトインのデバイスドライバを遅延ロードするために必要な手順は、次の通りです:

1.) OS Design のレジストリ設定ファイル(OSDesign.reg)において、遅延ロード対象のデバイスドライバの Flags の値を、DEVFLAGS_NOLOAD (0×00000004) を含むものに設定するよう記述を追加する(※common.reg などで Flags が設定されていれば、OSDesign.reg で上書き設定する)。

2.) 必要になった時点で、次の処理を実行する:
 2-1.) RegSetValueEx() を使って、そのドライバのレジストリの Flags の値を変更する(DEVFLAGS_NOLOAD を含まない値に変更する)。
 2-2.) そのドライバのレジストリキーを指定して ActivateDevice[Ex]() を呼び出す。
 2-3.) 再び RegSetValueEx() を使って、Flags の値を変更前のものに戻す。

上の手順のうち、(2) は、WEC の起動後に自動実行されるようにするのが簡単でしょう。遅延ロード対象のデバイスドライバ群に対して (2) を実行するアプリケーションを作り、そのアプリケーションを、/Windows/Startup/ ディレクトリに配置すれば、WEC が起動して、標準シェルのデスクトップ画面が表示された直後に、遅延ロード対象のデバイスドライバ群がロードされて動作開始します。この時、画面表示を見ているユーザにとっては、それらのデバイスドライバが起動時に自動ロードされる場合よりも起動時間が短縮されたように見えるでしょう。

■おまけ
/Windows/Startup/ ディレクトリに配置されたアプリケーションを自動起動する処理は、標準シェルのソースコード(※WEC 2013 には付属しません)でいうと、
 %_WINCEROOT%/public/shell/oak/hpc/explorer/main/explorer.cpp
で実装されている ProcessStartupFolder() で行われます。この関数が、SHGetSpecialFolderPath() で Startup ディレクトリのパス(つまり、/Windows/Startup/)を取得して、その中に入っている実行ファイル全てに対して順次 ShellExecuteEx() を呼び出す、という仕組みです。ProcessStartupFolder() は、DoStartupTasks() を介して、標準シェル(explorer.exe)のメインルーチンである WinMain() から呼び出されます。

explorer.exe の WinMain() では、標準シェルの初期化動作を行った後、CDesktopWnd クラスのインスタンスを生成して Create() を呼び出し、デスクトップ画面を表示します。さらに、CreateTaskBar() を実行するスレッドを生成・始動して、タスクバーを表示します。それらが済んだ後で、DoStartupTasks() を呼び出すようになっています。従って、遅延ロード対象のデバイスドライバをロードする処理は、/Windows/Startup/ に配置したアプリケーションで行うようにしておけば、標準シェルの初期画面が表示された後で遅延ロードが実行されるので、遅延ロードのぶん、初期画面が表示されるまでの時間が短縮される、というわけです。

標準シェルを使わず、カスタムシェルを使う場合(WEC 2013 のミニシェルを使う場合など)も、同じような方策で対応できるでしょう。WEC 7 をお持ちで、興味のある方は、explorer.cpp の内容を眺めてみると、面白いかも知れません。

Add comment 2015/09/16 koga

OS の起動時間短縮(1/2)~RAM レジストリの初期化時間を短縮する

■はじめに
今回と次回は、WEC の起動時間を短縮する方策を、二つ紹介します。WEC 2013 では、Snapshot Boot が導入され、適切に実装・チューニングすれば、OS の起動時間を数秒以内に抑えることも可能になりました。しかし、WEC 2013 よりも前の版では、Snapshot Boot を行うことができません。そこで、WEC 7 でも対応可能な起動時間短縮の方策を紹介してみることにしました。なお、Snapshot Boot については、MSDN の次のページをご覧ください。

 Snapshot Boot Development (Compact 2013)
 http://msdn.microsoft.com/en-us/library/dn169259.aspx

日本国内の Windows Embedded MVP 有志が主催して不定期に開催している、「Windows Embedded Community Day」の第1回で僕が担当したプレゼンでも、Snapshot Boot について簡単に紹介しています:

 組み込みでもマルチコア。WEC2013 での対応
 http://www.slideshare.net/ShinyaKoga/2013-0719wemvp-kansai

 Windows Embedded Community Day 第 1 回 – 真夏の組み込み Windows 技術セミナー2013
 http://msdn.microsoft.com/ja-jp/dn521045

■RAM レジストリの初期化
2011/08/02 に書いたエントリ(「レジストリの永続化~RAM-Based の場合」)では、レジストリの永続化対応を行う場合、Hive-Based のレジストリよりも、RAM-Based のレジストリの方が、より堅牢だと述べました。しかし、RAM-Based のレジストリには、起動時間が長くなるという短所があります。これは、レジストリの初期化に要する時間が、RAM-Based のレジストリの方が長いことが要因のようです。

Hive-Based のレジストリは、レジストリの Hive ファイルを filesys.dll が memory mapped file としてオープンして内容にアクセスします。つまり、Hive ファイルの内容は、filesys.dll が RAM 上に構築するレジストリ(一種のオンメモリデータベース)と同じではないかと思われます。従って、OS の起動時にレジストリを初期化する動作というのは、OS イメージに収録されている Hive ファイルや、内容更新されて永続化記憶域(ストレージ)に配置された Hive ファイルを、単に memory mapped file としてオープンするだけなのでしょう。

一方、RAM-Based のレジストリは、filesys.dll が RAM 上に構築するレジストリとは内容(フォーマット)が異なるようです。このため、OS の起動時にレジストリを初期化する際、ストレージからレジストリデータを読み出し、その内容を解析して RAM 上にレジストリを構築する、という処理が必要になります。このため、レジストリの初期化に要する時間が長くなるのです。この時間は、レジストリ全体のサイズに依存しますから、一般的には、OS イメージに組み込む機能が多いほど、つまり、OS Design のカタログ項目の選択が多いほど長くなります。10秒以上に達することがあることを確認していますし、永続化した場合は、レジストリの初期化時間の合計で20秒を超える場合もあります。

■永続化された場合の RAM レジストリの初期化
上で、「永続化した場合は」と書きました。そうです。RAM レジストリを永続化した場合、永続化しない場合よりも、レジストリの初期化に要する時間が長くなるのです。これについては、2011/08/02 に書いたエントリでも紹介した、WinCE 6.0 のリファレンスにある、次のページの説明にヒントがあります。

 File System Initialization of RAM-based Registry (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee490364(v=winembedded.60).aspx

このページには、RAM-based レジストリの初期化の流れが説明されているのですが、1から8まであるステップのうち、5と6は、次の通りです:

  1. Filesys.dll initializes the registry by checking for a registry in RAM.
    If Filesys.dll does not find a registry in RAM, it restores the registry from ROM. Filesys.dll loads \Windows\Default.fdf, which was created using Makeimg.exe.
  2. Filesys.dll checks for the ReadRegistryFromOEM OEM adaptation layer (OAL) function. If Filesys.dll does not find ReadRegistryFromOEM, it keeps the registry restored from ROM.

つまり、OS イメージ(ROM イメージ)に収録されている Default.fdf というファイル内容をロードして初期化した後、OAL の ReadRegistryFromOEM() を(それが実装されていれば)呼び出し、ストレージに保存されたレジストリデータをロードして初期化し直す、というわけです。つまり、RAM-Based のレジストリを永続化した場合、レジストリの初期化処理が実質二回行われるのです。

■永続化された RAM レジストリの初期化時間を短縮
RAM-Based のレジストリを永続化した場合に、永続化しない場合よりもレジストリの初期化に要する時間が長くなるのは、レジストリの初期化処理が二回行われるからなのです。これを、一回にすることができれば、そのぶん起動時間を短縮できることになります。何かよい方法はないでしょうか?

あります。それは、2012/01/13 に書いたエントリ(「OS の部分アップデート方策~その2(2/2)」)で述べた、ROM イメージを複数に分割することです。このエントリで、ROM イメージを複数の region に分割した場合、同じ名前(パス)のファイルに対して、region 間で shadowing が作用すると書きました(※「Region ごとの部分アップデート」という項をご覧ください)。この shadowing を利用するのです。

方策は、次の通りです:

1.) OSDesign.reg において、最小限の内容(たとえば、[HKLM\init] キーだけ)が有効になるように設定して OS イメージをビルドして、生成された Default.fdf ファイルを採取する。

2.) (1) で採取した Default.fdf ファイルのみを収録する ROM イメージの region を作成する。
 これは、config.bib によるメモリマップの設定と、ブートローダによる複数 region のロード、および、OAL による複数 region の連結処理(OEMRomChain のリスト構築処理)が必要です。

3.) 通常のビルド時は、(1) の設定を解除して OS イメージをビルドする。
 この結果、本来のレジストリ設定内容は、OS 本体を収録した region(NK region)に Default.fdf という内容で収録され、(1) で作成した(最少サイズの)Default.fdf は、それ専用の region に収録されます。

4.) OAL における OEMRomChain のリスト構築処理を実行する前に、ストレージ上にレジストリデータが存在するかどうかをチェックし、存在しない場合は、OEMRomChain のリスト構築処理において、Default.fdf のみを収録した region を無視する。ストレージ上にレジストリデータが存在する場合は、Default.fdf のみを収録した region を、OEMRomChain のリストにおいて、OS 本体を収録した region よりも前に配置する。

この方策により、ストレージ上にレジストリデータが存在する場合は、(1) で作成した最小限の Default.fdf ファイルの内容で一回目のレジストリ初期化が実行され、その時間を最小限(相対的に 0)にできます。その後、ストレージに保存されたレジストリデータがロードされ、レジストリが初期化される、というわけです。

ストレージ上にレジストリデータが存在しない場合は、(1) で作成した最小限の Default.fdf ファイルを収録した region は OEMRomChain のリストには挿入されませんから、shadowing が行われず、OS 本体を収録した region(NK region)に収録された、通常の Default.fdf がロードされてレジストリが初期化されます。従って、レジストリを一度もストレージに保存していない場合や、ストレージに保存したレジストリデータを無効化した場合にも、問題なく動作します。上記方策を適用しない場合の動作と比べて、起動時間が短縮される以外は、見た目の動作は変わりません。

Add comment 2014/09/23 koga

WEC のネットワークフィルタドライバ(NDIS フィルタ)

今回は、ネットワークドライバに対するフィルタドライバについて述べます。既存のドライバに対してフィルタドライバを attach することにより、ドライバの動作診断に役立てることができます。あるいは、どのように呼び出されるのかという視点からドライバの振る舞いを解析し、既存のドライバを改修する際に役立てたり、また、サンプルドライバの動作を解析して新規にドライバを実装する際の参考にする、といった利用もできるでしょう。

■はじめに~WEC 7 での NDIS 6.0 の導入
NDIS フィルタドライバは、WEC/WinCE で NDIS 6.0 に対応した版、つまり WEC 7 で導入されました。リファレンスにもサンプルコードにも、NDIS フィルタドライバが登場するのは WEC 7 からで、WinCE 6.0 にはありません。WEC 7 と WEC 2013 のリファレンスの、NDIS フィルタドライバの説明は、それぞれ次のページです:

 NDIS Functions for Filter Drivers (Compact 7)
 http://msdn.microsoft.com/en-us/library/gg158504(v=winembedded.70).aspx

 NDIS Filter Driver Reference (Compact 2013)
 http://msdn.microsoft.com/en-us/library/gg159366.aspx

NDIS 5.x だった WinCE 6.0 では、NDIS フィルタドライバに相当する機能は、NDIS 中間ドライバ(NDIS intermediate driver)として提供されていました。興味のある方は、WinCE 6.0 のリファレンスの次のページをご覧ください。

 Intermediate Drivers (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee483408(v=winembedded.60).aspx

NDIS の 5.x と 6.0 の違いについては、次のページが参考になるでしょう。

 Port Miniport Drivers from NDIS 5.x to 6.0 (Compact 7)
 http://msdn.microsoft.com/en-us/library/jj838857(v=winembedded.70).aspx

 Port Miniport Drivers from NDIS 5.x to 6.0 (Compact 2013)
 http://msdn.microsoft.com/en-us/library/jj838857.aspx

 Introduction to NDIS 6.0
 http://msdn.microsoft.com/en-us/library/windows/hardware/ff556026%28v=vs.85%29.aspx

■NDIS フィルタドライバのサンプルコード
NDIS フィルタドライバのサンプルコードは、WEC 7 も WEC 2013 も、次のディレクトリに収録されています:

 %_WINCEROOT%/public/common/oak/drivers/netsamp/lwfilter/

このサンプルコード(lwfilter)は、WEC 2013 のサンプルコードページにも WEC 7 のサンプルコードページにも、説明が載っていません。そのため、見落としていた方もいらっしゃるかも知れません。もし興味がわいたら、この機会に、ご覧になってみて下さい。lwfilter は、Windows Vista/7 用の NDIS 6.0 フィルタドライバのサンプルコードがベースになっているようで、同じ名前の Win7 用のサンプルコードが、次のページで提供されています:

 NDIS 6.0 Filter Driver
 https://code.msdn.microsoft.com/windowshardware/NDISLWFSYS-Sample-NDIS-60-42b76875

上述した、NDIS 5.x の NDIS 中間ドライバのサンプルコードは、WEC 7 以降でも提供されており、次のディレクトリに収録されています:

 %_WINCEROOT%/public/common/oak/drivers/netsamp/passthru/

NDIS 中間ドライバが WEC 7 以降も使えるのは、NDIS 5.x のサポートが含まれているからです。NDIS 5.x 関連のドライバインタフェースは、リファレンスの次のページで説明されています:

 NDIS 5.x Legacy Reference (Compact 7)
 http://msdn.microsoft.com/en-us/library/gg158312(v=winembedded.70).aspx

 NDIS 5.x Legacy Reference (Compact 2013)
 http://msdn.microsoft.com/en-us/library/gg158312.aspx

ちなみに、WEC 7 と WEC 2013 のネットワーク関連のサンプルコードは、次のページで紹介されています。興味のある方は、こちらもご覧になってみて下さい。

 Networking Code Samples (Compact 7)
 http://msdn.microsoft.com/en-us/library/hh802406(v=winembedded.70).aspx

 Networking Code Samples (Compact 2013)
 http://msdn.microsoft.com/en-us/library/hh802406.aspx

■フィルタドライバの効用
最後に、フィルタドライバの効用について述べます。冒頭で、既存のドライバの動作診断や、振る舞いの解析に役立てることができると書きましたが、その他に、既存のドライバに対する機能追加、という効用があります。NDIS は、フィルタドライバが attach されたドライバを直接呼び出さず、フィルタドライバを経由して間接的に呼び出します。従って、フィルタドライバは、既存のドライバに対する NDIS からの呼び出しをフックして追加の処理を実行できるのです。つまり、既存のドライバの実装を変更せずに、フィルタドライバによって機能追加を行うことができる、というわけです。

フィルタドライバにより機能追加を行うという方策は、機能追加したい対象のドライバが複数存在する場合に、より効果的です。たとえば、NDIS フィルタドライバによってドライバに機能拡張したいデバイスが、複数のネットワークインタフェースを持つ場合のことを考えてみて下さい。有線 LAN のインタフェースと無線 LAN のインタフェースを両方持つデバイスは、近年珍しくありません。それら複数のネットワークインタフェースのドライバに対して、同じ機能追加を行う場合、個々のドライバの実装を変更して機能追加するよりも、NDIS フィルタドライバを使って機能追加する方が、実装変更箇所が一つで済みますから、より低コストで実現できるでしょう。既存のドライバの実装を変更しなくて済むというのは、既存のドライバのソースコードを入手できず、自分で改変できない場合にも役立つ点です。

NDIS は、ネットワークインタフェースのハードウェアを抽象化したレイヤですから、そのレイヤのインタフェースに対するフィルタドライバは、異なるネットワークインタフェースのドライバに対して共通に適用できます。機能追加の例として、「ネットワーク通信が行われている間は、WEC の Power Manager の activity timer のイベントを発火させ、自動サスペンドしないようにする」という機能があります。この機能は、個々のネットワークインタフェースのドライバの実装を改変して実装するよりは、NDIS フィルタドライバで実装し、フィルタドライバを各ネットワークインタフェースのドライバに attach する方がシンプルで良いでしょう。これは、あくまでも一つの例に過ぎませんが、フィルタドライバの役立て方として参考になれば幸いです。

Add comment 2014/09/07 koga

WinCE/WEC の Secure Loader

前回(2013/02/10)から、一年ほど間が空いてしまいました。その間に、Windows Embedded Comact の最新版である Windows Embedded Comapct 2013 (WEC 2013) の一般提供も始まりました。この Blog でも、今後 WEC 2013 のことも交えて書く予定です。

ですが、今回は、前回の続編となることを書きます。組込み機器用の OS として WinCE/WEC が備えている、セキュリティを確保するための機能です。

■拡張性と脆弱性
前回は、ActivateDevice[Ex]() を用いてデバイスドライバを動的ロードすることができると説明しました。これは、便利な機能である反面、脆弱性の要因でもあります。

デバイスドライバを動的ロードできるということは、OS イメージに含めていないデバイスドライバを、OS の動作中に追加して、あらかじめ組み込まれてはいない機能を利用できるということです。たとえば、Flash メモリなどに配置した OS イメージを書き換えてアップデートする場合に、Flash メモリ用のデバイスドライバを OS イメージに組み込んでいなくとも、アップデート処理を実行するアプリケーションの .exe と、Flash メモリ用のデバイスドライバの .dll、および新しい OS イメージを USB メモリに入れて WEC デバイスにマウントし、アプリケーションを実行してアップデート処理を行うことが可能です。アップデート処理を実行するアプリケーションは、最初に ActivateDevice[Ex]() を使って、Flash メモリ用のデバイスドライバをカーネルにロードさせ、その後、DeviceIoControl() を使ってデバイスドライバを呼び出すことにより、OS イメージを Flash メモリに書き込む、というわけです。

デバイスドライバ、つまりカーネルモジュールを動的ロードできれば、このように拡張性を得られる一方で、悪意を持ったソフトウェアによる攻撃を容易にするという側面もあります。(悪意を持った)アプリケーションが可能な攻撃は、仮想メモリと実行モードによって制限されていますから、カーネルに対して致命的な障害を与えることは困難です。しかし、(悪意を持った)カーネルモジュールをアプリケーションがロードできてしまえば、カーネルのメモリ空間をアクセスできますし、プロセッサの特権モードで動きますので、カーネルに致命的な障害を引き起こすことは難しくありません。
(WinCE 5.0 までの純粋マイクロカーネルの場合には、デバイスドライバは、ユーザプロセスであるデバイスマネージャにロードされて動きますので、カーネルの保護は、より堅牢だったと言えます。)

■WinCE/WEC の Secure Loader
WinCE/WEC には、悪意を持ったソフトウェアによる攻撃を防ぐ機能として、Secure Loader (Loader Verifier Module) が用意されています。これは、あらかじめ OS イメージに組み込まれているアプリケーションや、認証されたアプリケーション以外は実行できないようにする仕組みです。WEC 7 のリファレンスでは、次のページで説明されています:

 Security Loader (Compact 7)
 http://msdn.microsoft.com/en-us/library/gg155695(v=winembedded.70).aspx

Secure Loader を有効にした OS イメージを作るには、OS Design のカタログ項目において、Loader Verifier Module を選択してビルドします。Loader Verifier Module は、Platform Builder のカタログ項目ビューにおいて、次の場所にあります:

 
  Core OS
   Windows Embedded Comapct
    Security
★    Loader Verifier Module

Secure Loader を有効にすると、OS イメージに組み込まれていないアプリケーションは、実行できなくなります。つまり、上で述べたデバイスドライバの動的ロードの例のように、USB メモリに入れたアプリケーションを実行しようとしても、実行できません。

OS イメージに組み込まれていないアプリケーションの実行を許さない処理は、WinCE/WEC のローダーによって行われます。ローダーが実行ファイル(executable)をロードする際に、ロードして構わないファイルかどうかをチェックして、条件に合わないものが指定された場合はエラーとするのです。

ローダーのソースコードは、
 C:/WINCE700/private/winceos/COREOS/nk/kernel/loader.c
にあります。loader.c で実装されている OpenExecutable() が、実行ファイルをロードする関数ですが、OpenExecutable() は、OS イメージに収録されたファイルに対しては OpenFileFromROM() を、ファイルシステム上のファイルに対しては OpenFileFromFilesys() を呼び出します。OpenFileFromROM() と OpenFileFromFilesys() は、FSOpenModule() という関数を呼び出して実行ファイルの内容をロードしますが、この FSOpenModule() において、ロードして構わないファイルかどうかのチェックが行われるようです。FSOpenModule() のソースは
 C:/WINCE700/private/winceos/COREOS/nk/kernel/fscall.c
にありますが、この関数は、filesys.dll を呼び出します。filesys.dll のソースは開示されていないため、詳細は分かりませんが、OpenFileFromROM() の場合、FSOpenModule() に渡す第二引数のフラグビット列で OPENMODULE_OPEN_ROMMODULE ビットが必ず 1 になるため、このビットを使って、OS イメージに収録されたファイルと、それ以外のファイルを区別して扱っているのだと思われます。

なお、Secure Loader 機能自体は、filesys.dll ではなく、lvmod.dll という DLL で実装されており、filesys.dll が lvmod.dll を呼び出すようです。カタログ項目の Loader Verifier Module を選択すると lvmod.dll が OS イメージに組み込まれることは、
 C:/WINCE700/public/common/oak/files/common.bib
を見ると分かります。Loader Verifier Module に関連付られた SysGen 変数は CE_MODULES_LVMOD ですので、これをキーにして common.bib を検索してみて下さい。

■署名による認証
Secure Loader を有効にすることにより、OS イメージに組み込まれていないアプリケーションを実行できないようになります。これでセキュリティは非常に高まりますが、拡張性は損なわれます。あらかじめ固定された機能しか動作しない、昔ながらの組込み機器であれば、ROM に配置した OS イメージに組み込まれたアプリケーションしか動かせなくても、問題ありません。しかし、サードパーティ製のアプリケーションを動かせるようにしたり、冒頭で述べたアップデート機能など、特定の場合にだけしか使わない機能を OS イメージには組み込まず、必要な時にだけ使いたい、という場合には、 OS イメージに組み込まれたアプリケーションしか実行できないのでは、対応できません。

ご安心下さい。WinCE/WEC の Secure Loader は、そのような場合に対応するための機能も備えています。実行ファイルに対する署名と、署名に対する認証機構により、OS イメージに組み込まれていないアプリケーションでも、認証できるものは実行を許すようになっています。上述した lvmod.dll が、この認証処理を行います。lmvod.dll が行う認証処理は、上述したローダーから呼び出される他、API としても提供されています。Secure Loader の API については、リファレンスの次のページをご覧ください:

 Security Loader Reference (Compact 7)
 http://msdn.microsoft.com/en-us/library/gg155306(v=winembedded.70).aspx

このリファレンスのページを見ると、ファイルを認証する API に加え、ブロックリスト、つまり、署名に対して認証エラーとするファイルの一覧を扱うものがあります。署名されており、署名に対する認証処理が成功するアプリケーション(実行ファイル)であっても、不正な動作をすることが分かった場合に、それをブロック対象として登録し、実行できないようにすることが可能なのです。

このように、不正なソフトウェアが実行されるのを防ぎ、セキュリティを確保するための仕組みを、拡張性と共に提供しているのが WinCE/WEC の Secure Loader というわけです。Secure Loader 用に、実行ファイルに署名を付ける手順は、リファレンスの次のページで説明されています:

 Signing Binaries (Compact 7)
 http://msdn.microsoft.com/en-us/library/gg156011(v=winembedded.70).aspx

 Deploy an Application on an OS with Security Loader (Compact 7)
 http://msdn.microsoft.com/en-us/library/jj200466(v=winembedded.70).aspx

WEC の最新版である WEC 2013 のリファレンスにも、同じ内容の説明があります:

 Signing Binaries (Compact 2013)
 http://msdn.microsoft.com/en-us/library/gg156011.aspx

 Deploy an Application on an OS with Security Loader (Compact 2013)
 http://msdn.microsoft.com/en-us/library/jj200466.aspx

■WinCE/WEC のセキュリティ
組込み機器のセキュリティは、一つの方策だけで確保できるものでは、ありません。機器の特性に応じて、複数の方策を組み合わせる必要があります。WinCE/WEC でのセキュリティの考え方や実現方策については、以下のページが参考になるでしょう。興味のある方は、ご覧になってみて下さい。

 Security for Windows Embedded Compact (Compact 7)
 http://msdn.microsoft.com/en-us/library/ee498894(v=WinEmbedded.70).aspx

 Trusted Environment Creation (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee498576(v=winembedded.60).aspx

 Windows Embedded CE 6.0 Security Model
 http://msdn.microsoft.com/en-us/library/ee497961(v=winembedded.60).aspx

 Security for Windows Embedded Compact Code Samples (Compact 7)
 http://msdn.microsoft.com/en-us/library/hh802407(v=winembedded.70).aspx

Add comment 2014/02/02 koga

insmod と ActivateDevice()

今回は、WEC/WinCE の、カーネルモジュールを動的にロード/アンロードする方法について述べます。Linux を御存知の方であれば、insmod や modprobe、rmmod に相当するものだといえば分かるでしょう。

■デバイスドライバの動的ロードとアンロード
Linux の insmod コマンドに相当するコマンドプログラムは、WEC/WinCE には存在しませんが、同様のことを実現可能な API が提供されています。それが、ActivateDevice[Ex]() です。

 ActivateDevice (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee484864.aspx

 ActivateDeviceEx (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee484469.aspx

これらの API を使ってデバイスドライバをロードする手順は、次の通りです:

1.) ロードするデバイスドライバを DeviceManager に登録するための、レジストリ項目を設定する。

2.) ActivateDevice[Ex]() を呼び出す。

デバイスドライバを使い終え、必要なくなったら、DeactivateDevice() を呼び出します。これで、カーネル内にロードされていたデバイスドライバの DLL がアンロードされます。

上の手順を insmod コマンドの場合と比べると、レジストリ設定が必要なぶん手順が一つ多いですが、十分シンプルだと言えるでしょう。レジストリに設定する項目は、そのドライバ用のサブキー配下の ‘Dll’ と ‘Prefix’ だけです。以下は、’MyDriver’ という名前で、デバイスファイル名に使われる3文字プレフィクスが ‘MYD’ というドライバの例です:


[HKEY_LOCAL_MACHINE\Drivers\AddOn\MyDriver]
   "Dll"="MyDriver.dll"
   "Prefix"="MYD"

上の例では、レジストリキー HKEY_LOCAL_MACHINE\Drivers の下に、動的ロード対象の意味で ‘AddOn’ というサブキーを割り当て、その下に、ドライバ用のサブキー ‘MyDriver’ を設定しています。

■LoadKernelLibrary()
ところで、Linux の insmod コマンドは、カーネルのローダブルモジュールをロードすることができ、対象はデバイスドライバに限定されていません。一方、WEC/WinCE の ActivateDevice[Ex]() は、DeviceManager が管理するデバイスドライバに対象が限定されています。

実は、WEC/WinCE にも、ローダブルモジュール(DLL)をカーネルにロードさせる API があります。LoadKernelLibrary というのが、その関数です。ただし、以下のリファレンスページで説明されているように、この API は、カーネルのログ機能を司る CeLog.dll と、カーネルデバッガの DLL をロードする用途に限定されています。実際には、それ以外の DLL もロードは可能だと思いますが、推奨はされないと考えて下さい。

 LoadKernelLibrary (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee478202.aspx

WEC/WinCE カーネルのログ機能と、ログデータの解析ツール(Kernel Tracker)については、2012/08/16 に書いたエントリ(「CeLogFlush.exe と Kernel Tracker」)で紹介しました。このエントリで、ログ機能を有効にしていないデバイスに対しても、一時的にログ機能を有効にできると書きました。CeLogFlush.exe は、LoadKernelLibrary() を呼び出すことにより、カーネルに CeLog.dll をロードさせるのです。

■動的ロードについて、もう少し
DLL をプロセスにロードする API として、LoadLibrary() がありますが、カーネル内部でも、デバイスドライバをロードする際に LoadLibrary() を呼び出す場合があります。これについては、ActivateDeviceEx() のリファレンスで説明されています。

通常は、ドライバのロードには LoadDriver() という関数が使われるのですが、ロードするドライバに対して ‘Flags’ というレジストリ項目が設定されていて、その値が 0×2 ビット(DEVFLAGS_LOADLIBRARY)を含んでいる場合は、LoadLibrary() が使われます。LoadLibrary() と LoadDriver() の違いは、ロード対象の DLL に対してデマンドページングを行うかどうかです。LoadDriver() は、ロードする DLL をデマンドページングの対象外とします。

WEC/WinCE カーネルのデマンドページングについては、リファレンスの次のページで説明されています。

 Demand Paging Considerations (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee482784(v=winembedded.60).aspx

時間制約の厳しいデバイスドライバの場合は、LoadDriver() によりデマンドページング無しでロードします。これが通常動作です。一方、サイズが大きく、かつ、時間制約も厳しくないドライバの場合には、LoadLibrary() を使うことにより、デマンドページング有りでロードすれば、メモリ使用量を抑えることが可能というわけです。

デバイスドライバにおいて、通常動作ではデマンドページング無しでロードするのは、リアルタイム性を確保するための仕組みです。WinCE 6.0 のリファレンスには、上のページを含む、“Real-Time Performance” という節があります。興味のある方は、ご覧になってみて下さい。

WEC 7 のリファレンスには、デマンドページングについて述べたページは見当たりませんが、割り込み応答動作のタイミングを計測するツール(ILTiming.exe)の説明などが載っています。こちらも、参考になるでしょう:

 ILTiming.exe Real-Time Measurement Tool (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee483144.aspx

Add comment 2013/02/10 koga

タスクバーの電源状態アイコンとバッテリドライバ

■電源管理ドライバとバッテリドライバ
WEC/WinCE の標準シェル(explorer)は、タスクバーに電源状態を示すアイコンを表示します。これは、デフォルトの動作であり、レジストリ設定で変更できます。タスクバーの電源状態アイコン表示に関するレジストリ設定は、リファレンスの次のページで説明されています。このページの、”Display Power Status” の項を見て下さい:

 Windows Embedded Compact Explorer Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee500839.aspx

ただし、OS の実装次第では、このアイコンは表示されません。このアイコンの表示には、電源管理ドライバとバッテリドライバが関係しており、それらの振る舞いによって、アイコンの表示動作が変わります。それぞれのソースコードは、WEC 7/WinCE 6.0 の次の場所にあります。

・タスクバーの電源アイコン表示
 %_WINCEROOT%/public/shell/oak/hpc/explorer/taskbar/power.{h,cpp}

・電源管理ドライバ
 %_WINCEROOT%/public/COMMON/oak/drivers/pm/

・バッテリドライバ
 %_WINCEROOT%/public/COMMON/oak/drivers/battdrvr/

電源管理ドライバとバッテリドライバは、どちらも二層構造の階層型ドライバ(layered driver)であり、MDD (Model Device Driver) と PDD (Platform Device Driver) で構成されています。階層型ドライバについては、WEC 7 のディベロッパーガイドにある次のページをご覧下さい:

 Layered and Monolithic Drivers (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/jj659821.aspx

PDD の実装は、プロセッサや CPU ボードの特性により、必要に応じてカスタマイズしますので、BSP (Board Support Package) にカスタマイズ部分が含まれます。カスタマイズの有無は BSP ごとに違いますが、WEC 7 付属の BSP ですと、Freescale i.MX313 のリファレンスボード用の BSP には、電源管理ドライバとバッテリドライバの両方のディレクトリが収録されています。次の場所です。

 %_WINCEROOT%/platform/IMX313DS/SRC/DRIVERS/BATTDRVR/
 %_WINCEROOT%/platform/IMX313DS/SRC/DRIVERS/PM/

この BSP の電源管理ドライバの方は、実は WEC 7 付属のものと違いません。上の PM/ ディレクトリには、ソースファイルは入っておらず、カスタマイズされていないのです。実際、sources ファイルを見ても、WEC 7 付属のデフォルト実装の .lib ファイルをリンクして pm.dll を生成する記述になっています。もしかすると、この BSP をもとにして電源管理ドライバをカスタマイズする開発者のために、テンプレートとして収録しているのかも知れません。

■電源状態の検出と表示
タスクバーに話を戻します。タスクバーは、現在の電源状態を表示に反映するために、電源管理ドライバが提供する電源状態監視機能を利用しています。電源管理 API の関数である RequestPowerNotifications() を呼び出すことにより、電源状態の変更をメッセージキューで受け取ることができます。そのメッセージキューに電源状態の変更通知が届いたことを検出すると、電源状態アイコンの表示を変更して通知内容を反映させるのです。

タスクバーのソースファイルの中で、電源管理ドライバから電源状態の変更通知を受け取る個所は、
 %_WINCEROOT%/public/shell/oak/hpc/explorer/taskbar/
ディレクトリの taskbar.cpp です。このソースファイルにある CTaskBar::MessageLoop() の中で、RequestPowerNotifications() の呼び出しと、MsgWaitForMultipleObjectsEx() を使ったメッセージループを実行します。メッセージループにおいて、MsgWaitForMultipleObjectsEx() を使ってウィンドウズメッセージのキューとメッセージキューを同時に監視して、届いたメッセージに対する応答動作を実行します。電源状態アイコン表示の変更は、冒頭で挙げた power.{h,cpp} で定義・実装されている PowerManagerUI クラスが担当します。

RequestPowerNotifications() のリファレンスは、次のページです:

 RequestPowerNotifications (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee497721.aspx

以上が、タスクバーと電源管理ドライバの連携の仕組みです。次に、電源管理ドライバとバッテリドライバの連携を見てみましょう。

電源管理ドライバは、バッテリドライバから PowerPolicyNotify() の呼び出しによって電源状態の通知を受け取ります。PowerPolicyNotify() によって、電源管理ドライバが読み出すメッセージキューに通知メッセージが投入され、その結果、タスクバーなど、電源状態監視機能のクライアントへ通知されるというわけです。

PowerPolicyNotify() のリファレンスは、次のページです:

 PowerPolicyNotify (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee497744.aspx

PowerPolicyNotify() のソースコードは、
 %_WINCEROOT%/private/winceos/COREOS/core/thunks/tpm.cpp
にあり、この関数によってメッセージが投入されるメッセージキューから読み出しを行う処理は、
 %_WINCEROOT%/public/common/oak/drivers/pm/pdd/common/pddpolicy.cpp
 %_WINCEROOT%/public/common/oak/drivers/pm/pdd/default/pwsdef.cpp
で実装されています。興味がある方は、ご覧になってみて下さい。PmPolicyReadNotificationQueue() というのが、pdbpolicy.cpp で実装されている、メッセージキューから読み出しを行う関数です。

■バッテリドライバによる電源状態監視
バッテリドライバは、電源状態を監視して、状態変化を検出した際に電源管理ドライバへ通知しますが、そのためにスレッドを起動します。このスレッドが実行する手続きは、
 %_WINCEROOT%/public/common/oak/drivers/battdrvr/mdd/battdrvr.c
で実装されている BatteryThreadProc() です。このスレッドを生成する際、CreateThread() の第5引数(fdwCreate)に CREATE_SUSPENDED を渡しており、サスペンド状態で生成されます。従って、ResumeThread() を呼び出すまでは、このスレッドは動作しません。

このスレッドに対して ResumeThread() が呼び出されるのは、バッテリドライバに対して IOCTL_BATTERY_POSTINIT という制御コードで DeviceIoControl() が呼び出された場合です。それまでの間は、バッテリドライバ内の電源状態監視スレッドはサスペンド状態で生成されたままとなり、電源状態の監視動作は行われません。この結果、タスクバーには、電源状態アイコンが表示されないのです。

今回のエントリの冒頭で、「ただし、OS の実装次第では、このアイコンは表示されません。」と書きましたが、それは、このことを指したものなのです。つまり、OS の起動時にバッテリドライバ(デバイス名は BAT1:)に対して IOCTL_BATTERY_POSTINIT の DeviceIoControl() を呼び出さないと、OS が起動してタスクバーが表示された直後に、AC 電源接続を示すアイコンが表示されたのち、そのアイコンの表示は消えてしまいます。

バッテリドライバに対して IOCTL_BATTERY_POSTINIT の DeviceIoControl() を呼び出さないと(つまり、バッテリドライバの電源状態監視スレッドを始動しないと)電源状態アイコンの表示が消えてしまうのは、電源管理ドライバが持っている電源状態のキャッシュが、無効な内容で初期化されたままになるからです。このキャッシュは、
 %_WINCEROOT%/public/common/oak/drivers/pm/pdd/default/platform.cpp
で宣言されている gSystemPowerStatus という名前の、POWER_BROADCAST_POWER_INFO 型の大域変数です。gSystemPowerStatus は、
 C:\WINCE700\public\common\oak\drivers\pm\pdd\common\pddpower.cpp
で実装されている PmInitPowerStatus() で全てのフィールドを無効値(0xFF)に初期化されます。そのため、バッテリドライバの電源状態監視スレッドがサスペンドしたままだと、キャッシュの内容は更新されず、RequestPowerNotifications() の呼び出しに対して最初に一回だけ送付する電源状態通知の内容が無効値となります。その結果、タスクバーが電源状態アイコンの表示を消すのです。

バッテリドライバに対して IOCTL_BATTERY_POSTINIT の DeviceIoControl() 呼び出しを行うと、バッテリドライバの電源状態監視スレッドが一定間隔(デフォルトは5秒)で電源状態の取得動作を実行しますので、電源管理ドライバが持つ電源状態のキャッシュが正しい内容となります。すると、タスクバーに電源状態アイコンが表示されます。

バッテリドライバの電源状態監視スレッドが、デフォルト実装では停止したままになるのは、不要にスレッドを動かして CPU リソースを消費することを避けるためなのでしょう。電源状態監視スレッドは、バッテリ駆動のハードウェアで OS が動作する場合にのみ動作させればよいわけです。デフォルト実装では AC 電源接続としていますから、電源状態監視スレッドを始動しないようになっているのではないかと思います。

■電源状態アイコン表示のレジストリ設定
最後に、レジストリ設定について補足します。バッテリ駆動のハードウェアで動かす場合に、バッテリドライバをカスタマイズして、電源状態の監視処理の実装を行ったとしましょう。そして、OS の起動時に自動実行されるアプリケーションなどを使って、バッテリドライバの電源状態監視スレッドを始動するようにした場合です。

バッテリドライバによって、電源監視状態の監視は行うが、タスクバーに電源状態アイコンを表示したくない、という場合は、レジストリ設定を変更する必要があります。デフォルトでは、バッテリドライバの電源監視スレッドが動作すると電源状態アイコンが表示されるからです。電源状態アイコンをタスクバーに表示させないためのレジストリ設定は、次の二通りです:

 ・HKEY_LOCAL_MACHINE\Software\Microsoft\Power キーの ShowIcon の値に 0 を設定する。

 ・HKEY_LOCAL_MACHINE\Software\Microsoft\Power キーのみをレジストリに登録して、ShowIcon は設定しない。

ShowIcon の値は、デフォルト値が 0 として扱われるため、HKEY_LOCAL_MACHINE\Software\Microsoft\Power キーの下に ShowIcon が存在しなければ、0 として扱われます。ただし、HKEY_LOCAL_MACHINE\Software\Microsoft\Power キーそのものが存在しない場合は、デフォルト値として 1 が使われるのです。バッテリドライバのデフォルトの設定では、HKEY_LOCAL_MACHINE\Software\Microsoft\Power キーがレジストリに登録されないため、ShowIcon の値が 1 として扱われ、電源状態アイコンが表示されます。

Add comment 2013/01/20 koga

WEC 7/WinCE 6.0 のブートシーケンス~init に至るまで

■はじめに
前々回のエントリ(「Telnet サーバと udevice.exe(2/2)」)で、User Mode Driver Framework の Reflector が、カーネルランドからユーザランドを呼び出す仕組みを紹介した際、カーネルのソースコードを少しだけ覗いてみました。今回は、WEC 7/WinCE 6.0 のブートシーケンスについて、再びカーネルのソースコードを覗いてみます。

もしあなたが、WEC 7/WinCE 6.0 のカーネル移植をこれから始めてみようと思っていらっしゃるのであれば、少しだけ参考になるかも知れません。あるいは、カーネル移植を自分で手がけることはないけれど、どのような手順で WEC 7/WinCE 6.0 がブートするのか興味がある、という方にも、楽しんでもらえると良いなと思います。Linux などの UNIX 系 OS では、カーネルが最初に init プロセスを生成・起動し、init が全てのプロセスの母となりますが、WEC 7/WinCE 6.0 で init に相当するものは何なのかも、見てみます。

■WEC 7/WinCE 6.0 のブートシーケンス(ARM の場合)
分かりやすく書く、というのは、難しいことです。今回は、簡潔に、カーネルのソースコードの具体的な個所を示して、ブートシーケンスの大枠だけを書いてみます。興味のある方は、ご自分でソースコードを追ってみて下さい。正確に理解するには、それが一番だと思います。説明の都合上、ここでは ARM プロセッサの場合のブートシーケンスについて記します。他のプロセッサ(x86, MIPS)の場合も、大枠は違わないでしょう。

まず最初に実行されるのは、OS イメージの先頭に配置されるエントリルーチンの、StartUp() です。StartUp() は、startup.s というソースファイルにアセンブラで実装されており、プロセッサの必要最小限の初期化処理を行います。具体的には、(プロセッサをスーパバイザモードに設定した後で)割り込みコントローラと MMU をディゼーブルにして、キャッシュと RAM を初期化します。また、デバッグ用の LED 点灯などのために、必要最小限の周辺機器コントローラの初期化も行います。startup.s は、WEC 7/WinCE 6.0 に付属する BSP ですと、たとえば次の場所にあります:

・Freescale i.MX27 の BSP(WEC 7)
 %_WINCEROOT%/platform/3DS_iMX27/src/OAL/oallib/startup.s

・デバイスエミュレータの BSP(WinCE 6.0)
 %_WINCEROOT%/platform/DEVICEEMULATOR/src/oal/oallib/startup.s

エントリルーチンの StartUp() を実装している startup.s は、実はカーネルとブートローダに共通のソースファイルとなっています。StartUp() は、初期化処理を行った後に KernelStart() という手続きを呼び出すのですが、カーネルとブートローダのそれぞれにおいて、KernelStart() が実装されています。つまり、カーネルとブートローダでは異なる起動処理の本体を、同じ名前の KernelStart() という手続きで実装することにより、それを呼び出すエントリルーチンを共通化している、というわけです。

カーネルの方の KernelStart() は、nkldr.lilb という静的リンクライブラリに所属する armstart.s で実装されており、armstart.s は、
 %_WINCEROOT%/private/winceos/COREOS/nk/ldr/arm/
に収録されています。ここで、nkldr.lib は oal.exe にリンクされるライブラリです。oal.exe は、
 %_WINCEROOT%/public/common/oak/files/common.bib
にある次の行の設定により、nk.exe という名前で OS イメージに格納されます。この nk.exe(oal.exe)が、カーネル移植レイヤを含む中核であり、ブートシーケンスに最初から関わるのです。


MODULES
;  Name            Path                                           Memory Type
;  --------------  ---------------------------------------------  -----------
; @CESYSGEN IF CE_MODULES_NK
IF IMGNOKITL
    nk.exe          $(_FLATRELEASEDIR)\oal.exe                  NK  SHZ
ENDIF IMGNOKITL

IF IMGNOKITL !
IF IMGNOKITLDLL
    nk.exe          $(_FLATRELEASEDIR)\oalkitl.exe              NK  SHZ
ENDIF IMGNOKITLDLL
IF IMGNOKITLDLL !
    nk.exe          $(_FLATRELEASEDIR)\oal.exe                  NK  SHZ
    kitl.dll        $(_FLATRELEASEDIR)\kitl.dll                 NK  SHZ
ENDIF IMGNOKITLDLL !
ENDIF IMGNOKITL !

KernelStart() が呼び出された後のブートシーケンスは、ARM プロセッサの場合、次の通りです:

1.) KernelStart() は、OEMAddressTable に記述されたカーネル仮想アドレスのマップに従って、MMU のページテーブルを設定したのち、MMU とキャッシュをイネーブルにして仮想アドレスに遷移する。また、割り込みハンドラのスタックを設定する。

2.) その後、KernelStart() は、ARMInit() を呼び出して kernel.dll のエントリルーチンのアドレスを取得する。そして、そのアドレス(kernel.dll のエントリルーチンである NKStartup() の開始アドレス)へジャンプする。
  ここで、ARMInit() は FindKernelEntry() という関数を使って kernel.dll のエントリルーチンのアドレスを取得します。ARMInit() と FindKernelEntry()、そして NKStartup() は、それぞれ次のソースファイルで実装されています。

 - ARMInit() :nk.exe に所属
  %_WINCEROOT%/private/winceos/COREOS/nk/ldr/arm/arminit.c

 - FindKernelEntry() :nk.exe に所属
  %_WINCEROOT%/private/winceos/COREOS/nk/ldr/ldrcmn.c

 - NKStartup() :kernel.dll に所属
  %_WINCEROOT%/private/winceos/COREOS/nk/kernel/arm/mdarm.c

3.) NKStartup() が、(nk.exe のではなく)kernel.dll の KernelStart() を呼び出す。
  kernel.dll の KernelStart() を実装しているソースファイルは、
   %_WINCEROOT%/private/winceos/COREOS/nk/kernel/arm/armtrap.s
  です。

4.) KernelStart() が、KernelInit() を呼び出す。KernelInit() によって、カーネルのヒープと仮想記憶管理構造、および、プロセス管理構造とスレッド管理構造が初期化される。
  ここで、スレッド管理構造を初期化する THRDInit() という関数では、SystemStartupFunc() という関数を、最初に起動するスレッドが実行する手続きとしてセットします。KernelInit() と THRDInit() は、それぞれ次のソースファイルで実装されています。

 - KernelInit()
  %_WINCEROOT%/private/winceos/COREOS/nk/kernel/nkinit.c

 - THRDInit()
  %_WINCEROOT%/private/winceos/COREOS/nk/kernel/thread.c

5.) その後、KernelStart() は Reschedule() を呼び出す。Reschedule() によって、最初のスレッドに実行が移って動き出し、(4) で設定された SystemStartupFunc() を実行する。
  Reschedule() も、armtrap.s で実装されています。THRDInit() によってマルチスレッド機構が構築されて、最初の Reschedule() の呼び出しによって、マルチスレッドモードへ遷移するというわけです。

6.) SystemStartupFunc() が、KernelInit2() を呼び出して、マルチスレッドモードでなければ行えないカーネルの初期化処理を実行する。
  SystemStartupFunc() は、さらに、ローダーとページプールの初期化、および、(それらが組込まれていれば)カーネルのロギング機構やカーネルデバッガの初期化も行います。メッセージキューやウォッチドッグタイマの初期化も、ここで行われます。SystemStartupFunc() を実装しているソースファイルは、
   %_WINCEROOT%/private/winceos/COREOS/nk/kernel/schedule.c
です。KernelInit2() のソースファイルは、nkinit.c です。

7.) SystemStartupFunc() は、各種初期化動作を行った後、ブートシーケンスの最終段階として、RunApps() という関数を実行するスレッドを起動する。
  RunApps() を実行するスレッドを起動した後、SystemStartupFunc() は、RTC のアラーム発火を待つ無限ループを実行します。

8.) RunApps() が、filesys.dll の WinMain() を実行するスレッドを起動する。
  その名前から連想する動作とは違い、RunApps() は、プロセス(アプリケーション)を起動するのではなく、スレッドを起動します。純粋マイクロカーネル構造だった WinCE 5.0 までは、filesys は DLL ではなく EXE でしたから、その名残なのかも知れません。
  filesys.dll のメインルーチン(WinMain())を実行するスレッドを起動した後、RunApps() は、CleanPagesInTheBackground() という関数を実行します。CleanPagesInTheBackground() は、
   %_WINCEROOT%/private/winceos/COREOS/nk/kernel/physmem.c
  で実装されており、自身の優先度を最低優先度値(255)に変更した後、不要になったページを破棄する無限ループを実行します。つまり、Linux でいえば swapper に相当するアイドルスレッドとなります。

以上が、WCE/WinCE カーネルのブートシーケンスの大枠です。ブートの最終段階として、filesys.dll のメインルーチンが実行されることが分かりました。
filesys.dll は、起動後(つまり、メインルーチンの実行開始後)、レジストリの初期化とファイルシステム API の初期化を行い、続いて、Storage Manager と Device Manager (device.dll) を起動します。device.dll によってデバイスドライバがロードされ、Storage Manager によって、ファイルシステム上のファイルを読み書きできるようになります。その後、filesys.dll は、レジストリの HKEY_LOCAL_MACHINE\Init キー配下に記述された設定内容に従って、プロセスを起動します。標準シェル(explorer.exe)が起動されるのは、このタイミングです。

filesys.dll のブート時動作については、WinCE 6.0 のリファレンスで説明されています:

 File System Boot Process (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee490357(v=winembedded.60).aspx

この Blog の以前のエントリでも、Storage Manager と device.dll の起動順序について書きました。興味のある方は、こちらもどうぞ:

 レジストリ変更内容の永続化(2/2)
 http://www.stprec.co.jp/ceblog/2011/02/21/

さて、filesys.dll が、ブートの完了時に、レジストリの HKEY_LOCAL_MACHINE\Init キー配下の記述に従ってプロセスを起動すると書きました。ということは、filesys.dll が全てのプロセスの親となるわけです。つまり、UNIX 系 OS の init プロセスに相当します。init プロセスとは異なり、filesys.dll は、カーネルにロードされる DLL ですが、WinCE 5.0 までは、filesys もプロセスでした。こうやって見てみると、ブートシーケンスの大枠は、UNIX 系 OS と *大きくは* 違わないことが分かります。

■KernelRelocate() について
WEC/WinCE のブートシーケンスについて、もう少しだけ補足します。上に書いたブートシーケンスの (2) に登場した、ARMInit() です。

ARMInit() は、FindKernelEntry() を呼び出す前に、KernelRelocate() という関数を呼び出します。この KernelRelocate() を呼び出すことによって、初めて大域変数へアクセスできるようになるのです。大域変数は、実行形式ファイル(.exe と .dll)の data セクションに配置されますが、OS イメージは ROM 領域にあるので、それを RAM 領域へ再配置(relocate)する必要があります。この再配置は、LoadLibrary() や CreateProcess() によってロードされる場合は、カーネルのローダーによって行われます。しかし、上で述べたブートシーケンスにおける、nk.exe(oal.exe)と kernel.dll の呼び出しの流れでは、(6) で SystemStartupFunc() がローダーを初期化する前に行われます。従って、ローダーは介在できず、自分自身で再配置処理を行われなければいけません。そのための関数が KernelRelocate() なのです。

KernelRelocate() による再配置処理の詳細は、MSDN の Blog サイトにある、WinCE の開発チームの人による Blog エントリに書かれています:

 How does Windows Embedded CE 6.0 Start?
 http://blogs.msdn.com/b/ce_base/archive/2007/11/26/how-does-windows-embedded-ce-6.0-start_3f00_.aspx

上のページで ‘KernelRelocate’ を検索してみて下さい。なお、ここの説明には、そこまでは書かれていないのですが、おそらく、romimage.exe が OS イメージファイル(nk.bin)を生成する際、”copy entries” として TOC に記述されるのは(※この copy entries を KernelRelocate() が参照します)、oal.exe と kernel.dll の data セクションだけではないかと思います。それら以外の EXE や DLL の data セクションは、ローダーによってロード時に再配置が行われるはずですし、必ずロードされるとは限らないものを、ブート時に全て RAM に配置するのは無駄があるからです。

ローダーによる、ROM 領域の実行形式ファイルに対する再配置処理は、
 %_WINCEROOT%/private/winceos/COREOS/nk/kernel/loader.c
で実装されている PageInOnePage() の中で行われるように見えます。もし興味のある方は、ご自分で追ってみて下さい。WEC/WinCE の実行形式ファイルフォーマットについては、PE (Portable Executable) のフォーマットを解説したページが参考になります:

 EXEファイルの内部構造(PEヘッダ)
 http://codezine.jp/article/detail/412?p=2

 Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
 http://msdn.microsoft.com/en-us/library/ms809762.aspx

Add comment 2012/12/21 koga

Telnet サーバと udevice.exe(2/2)

前回(2012/08/27)から、だいぶ間が空いてしまいましたが、続きです。

■サービスとユーザモードドライバの起動の仕組み
前回の説明で、telneted や ftpd などのサービスは、それらの DLL のホストプロセスが servicesd.exe であり、ユーザモードドライバのホストプロセスは udevice.exe だということを述べました。そして、各サービスは、ユーザモードのデバイスドライバとして実装されており、そのため、サービスのホストである servicesd.exe と、通常のユーザモードドライバのホストである udevice.exe は、中核部の実装を共有していることも説明しました。

今回は、両者について、もう少し詳しく見てみます。

まず、サービスとユーザモードドライバが、それぞれ、どのようにしてロードされて起動するのかを見てみましょう。両者ともに、OS の起動時に自動的に起動するように設定することもできれば、アプリケーションからの要求に応じて起動することもできます。

(OS の起動時に自動起動されない設定の)ユーザモードドライバを起動するには、カーネルモードのドライバと同じく、ActivateDevice() または ActivateDeviceEx() を使います。前回のエントリでも紹介したリファレンスのページに書かれている通り、ActivateDevice() や ActivateDeviceEx() の第一引数に渡すドライバのレジストリキーにおいて、Flags の値が DEVFLAGS_LOAD_AS_USERPROC ビット(0×10)を含んでいると、そのドライバは、カーネルではなく、udevice.exe プロセスによってロードされ、ユーザ空間で動作します。

一方、サービスを起動するには、ActivateService() という関数を使います:

 ActivateService (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee501277.aspx

ActivateDevice[Ex]() に渡すレジストリキーは、HKEY_LOCAL_MACHINE\Drivers\ 配下のサブキーですが、ActivateService() に渡すレジストリキーは、HKEY_LOCAL_MACHINE\Services\ 配下のサブキーです。つまり、レジストリ設定においては、ユーザモードドライバとサービスは、HKEY_LOCAL_MACHINE\Drivers\ 配下に記述されるのか HKEY_LOCAL_MACHINE\Services\ 配下に記述されるのかによって区別されます。この区別は、OS の起動時における自動起動処理においても使われます。

■デバイスドライバ(ユーザモードドライバ)の自動起動処理
OS 起動時のデバイスドライバの自動起動処理は、デバイスマネージャの働きによって行われます。具体的には、次の手順で、レジストリキー KEY_LOCAL_MACHINE\Drivers\BuiltIn\ 配下に記述されたビルトインのドライバ群が起動されます。この起動手順は、カーネルモードドライバとユーザモードドライバで共通です。上述したように、各ドライバのレジストリキーにおいて、Flags キーの値に DEVFLAGS_LOAD_AS_USERPROC ビットを含むドライバが、ユーザモードドライバとしてロードされます。

1.) デバイスマネージャ(device.dll)のエントリルーチンである DevMainEntry() において、StartDeviceManager() を呼び出す。
2.) StartDeviceManager() は、DevLoadInit() を呼び出すことにより、ブートの第1フェーズでロードされるデバイスドライバ群をロードして起動する。
3.) StartDeviceManager() は、その後、InitDevices() を呼び出して、ブートの第2フェーズでロードされるデバイスドライバ群をロードして起動する。

上の (2) および (3) において、レジストリキー HKEY_LOCAL_MACHINE\Drivers\BuiltIn を指定して ActivateDevice() または ActivateDeviceEx() が呼び出されます。その結果、指定されたレジストリキー配下のドライバ群を数え上げて起動する BusEnum という特殊なドライバが起動し、このドライバが、HKEY_LOCAL_MACHINE\Drivers\BuiltIn\ 配下に記述されたドライバ群をロードして初期化します。

ここで、「ブートの第1フェーズ」と「第2フェーズ」については、このブログの 2011/02/21 のエントリ(「レジストリ変更内容の永続化(2/2)」)をご覧下さい。

BusEnum は、ブートの第1フェーズと第2フェーズのそれぞれにおいてインスタンスが生成されます。以下に、ブートの第1フェーズにおいて BusEnum が生成・起動された時の呼び出し履歴(コールスタック)を示します。興味のある方は、これを手掛かりにして、デバイスマネージャのソースコードをご覧になるのも面白いでしょう:

 BUSENUM!BusEnum::BusEnum()
 BUSENUM!Init()
 DEVMGR!DriverFilterMgr::DriverInit()
 DEVMGR!DeviceContent::EnableDevice()
 DEVMGR!DeviceContent::InitialEnable()
 DEVMGR!I_ActivateDeviceEx()
 DEVMGR!DM_ActivateDeviceEx()
 K.COREDLL!xxx_ActivateDeviceEx()
 DEVMGR!InitDevices()
 DEVMGR!DevloadInit()
 DEVMGR!StartDeviceManager()
 DEVICE!DevMainEntry()
 K.COREDLL!ThreadBaseFunc()

注意:ただし、ユーザモードドライバは、ブートの第1フェーズではロードできません。ユーザモードドライバをロードできるのは、ブートの第2フェーズおよび、ブート完了後です。

■サービスの自動起動処理
サービスの自動起動処理は、serviceStart.exe によって行われます。serviceStart.exe は、レジストリキー HKEY_LOCAL_MACHINE\init\ 下に登録され、device.dll 、つまりデバイスマネージャの起動完了後に起動されるように設定されています。具体的には、次の手順で、レジストリキー HKEY_LOCAL_MACHINE\Services\ 配下に記述されたサービス群が起動されます。

1.) serviceStart.exe は、レジストリキー HKEY_LOCAL_MACHINE\Services を指定して ActivateDevice() または ActivateDeviceEx() を呼び出す。

HKEY_LOCAL_MACHINE\Services を指定した ActivateDevice[Ex]() の呼び出しの結果、このレジストリキー配下のサービス群を数え上げて起動する ServicesEnum という特殊なドライバが起動し、ServicesEnum が、HKEY_LOCAL_MACHINE\Services\ 配下に記述されたサービス群をロードして初期化します。

この動作は、”HKEY_LOCAL_MACHINE\Drivers\BuiltIn” が “HKEY_LOCAL_MACHINE\Services” に代わり、そして BusEnum が ServiceEnum に代わった以外は、デバイスドライバの場合と概ね同じです。デバイスドライバ(ユーザモードドライバ)やサービスをロードするホストプロセスは、ユーザモードドライバが udevice.exe でサービスが servicesd.exe という違いはありますが、大枠の処理の流れは同じです。そのため、実装上も、前回のエントリから述べてきたように、udevice.exe と servicesd.exe が中核機能を共有している、というわけです。

以下に、serviceStart.exe から ServiceEnum が呼び出された時のコールスタックを示します:

 SERVICESENUM!ServicesEnum::ServicesEnum()
 SERVICESENUM!Init()
 DEVMGR!DriverFilterMgr::DriverInit()
 DEVMGR!DeviceContent::EnableDevice()
 DEVMGR!DeviceContent::InitialEnable()
 DEVMGR!I_ActivateDeviceEx()
 DEVMGR!EX_DM_ActivateDeviceEx()
 COREDLL!xxx_ActivateDeviceEx() ★
 SERVICESSTART!WinMain()
 SERVICESSTART!WinMainCRTStartupHelper()
 SERVICESSTART!WinMainCRTStartup()
 COREDLL!MainThreadBaseFunc()

ちなみに、上のコールスタックで★を付けた行の、coredell.dll の ActivateDeviceEx() の呼び出しまでがユーザランドで、それより上、つまり、devmgr.dll の EX_DM_ActivateDeviceEx() 以降は、カーネルランドにおける呼び出しです。ActivateDeviceEx() から EX_DM_ActivateDeviceEx() の間には、システムコールが介在しているのですが、カーネルデバッガは、その遷移を通常の関数呼び出しのように見せてくれるのです。

■ホストプロセスを指定する仕組み
次に、ユーザモードドライバやサービスが、どのようにしてホストプロセスに割り当てられるのかを見てみます。前回のエントリで紹介した、WinCE 6.0 のリファレンスにある User Mode Driver Framework のアーキテクチャ図を見て下さい。

 User Mode Driver Framework Architecture (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee486510(v=winembedded.60)

この図にある Reflector が、ユーザモードドライバやサービスをホストプロセスに割り当てる処理を実行します。Reflector を呼び出すのは、上のページのアーキテクチャ図にある通り、デバイスマネージャです。

デバイスマネージャは、デバイスドライバの DLL をロードするよう要求された際、その DLL に対して DEVFLAGS_LOAD_AS_USERPROC が指定されている場合は、Reflector のインスタンスを生成します。サービスの場合も、同様にデバイスマネージャに対して DLL のロードが要求され、その際、DEVFLAGS_LOAD_AS_USERPROC が指定されます。その結果、ユーザモードドライバに対してもサービスに対しても、それらの DLL ごとに Reflector のインスタンスが生成されて、DLL に割り当てられます。そして、デバイスマネージャは、Reflector を介して、ユーザモードドライバやサービスとやり取りします。つまり、Reflector が proxy の役割を担います。

Reflector は、自身に割り当てられた DLL をロードさせるホストプロセスを探し、存在しない場合は、それを起動します。DLL をロードさせるホストプロセスが何かというのは、レジストリの設定によって決まります。具体的には、ユーザモードドライバやサービスのレジストリキーにおける UserProcGroup の値によって、ホストプロセスの実体が決まります。WEC 7 のリファレンスですと、次のページに説明があります:

 User Mode Driver Framework Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee482921.aspx

ただし、上のページには、UserProcGroup ではなく ProcGroup と記載されています。これは間違いだと思われます。実際、後述する
 %_WINCEROOT%/public/common/oak/files/common.reg
では、ホストプロセスの ID を指す値は UserProcGroup となっています。また、’ProcGroup’ という値を指定しても、その値は無視されてしまいます。Reflector のソースファイルにある CreateReflector() の実装を見ても、’UserProcGroup’ という値を参照していますから、上のページの説明が間違っているのだと思います。

Reflector によって生成されたホストプロセスの情報は、デバイスマネージャ内の大域変数に束縛された連結リストに格納され、それぞれのホストプロセスには ID が付けられます。そして、UserProcGroup で指定された ID のホストプロセスが既に生成済みであれば、そのプロセスを呼び出して、ユーザモードドライバやサービスの DLL をロードさせます。UserProcGroup で指定された ID のホストプロセスが、未だ生成されていなければ、生成したうえで、DLL をロードさせます。これらの処理の詳細に興味のある方は、Reflector のソースコードをご覧になってみて下さい。Reflector のソースファイルは、
 %_WINCEROOT%/private/winceos/COREOS/device/devcore/reflector.cpp
です。

参考までに、serviceStart.exe によってサービス群がロード・起動される際の、一つのサービスに対して Reflector が生成されるまでの呼び出しのコールスタックを以下に示します:

 DEVMGR!CreateReflector()
 DEVMGR!Reflector_Create()
 DEVMGR!DeviceContent::LoadLib()
 DEVMGR!I_ActivateDeviceEx()
 DEVMGR!DM_ActivateDeviceEx()
 K.COREDLL!xxx_ActivateDeviceEx() ★★
 SERVICESENUM!DeviceFolder::LoadDevice()
 SERVICESENUM!ServicesEnum::ActivateAllChildDrivers()
 SERVICESENUM!ServicesEnum::PostInit()
 SERVICESENUM!DefaultBusDriver::FastIOControl()
 SERVICESENUM!ServicesEnum::FastIOControl()
 SERVICESENUM!DefaultBusDriver::IOControl()
 SERVICESENUM!IOControl()
 DEVMGR!DriverFilterMgr::DriverControl()
 DEVMGR!DriverControl()
 DEVMGR!IoPckManager::DevDeviceIoControl()
 DEVMGR!DevDeviceIoControl()
 DEVMGR!DM_DevDeviceIoControl()
 KERNEL!MDCallKernelHAPI()
 KERNEL!NKHandleCall()
 K.COREDLL!DirectHandleCall()
 K.COREDLL!xxx_DeviceIoControl()
 DEVMGR!DevicePostInit()
 DEVMGR!DeviceContent::EnableDevice()

devmgr.dll の DeviceContent::EnableDevice() が呼び出されるまでの経路は、前項の「サービスの自動起動処理」に示したコールスタックと同じですから、ここでは省略します。上のコールスタックでは、★★を付けた行に注目して下さい。ServiceEnum.dll の DeviceFolder::LoadDevice() から ActivateDeviceEx() が呼び出されています。実は、サービスに対しても、デバイスマネージャ内部では、ロードする際には ActivateDeviceEx() が呼び出されるのです。これは、アプリケーションから ActivateService() を呼び出してサービスを起動する場合も同じです。ActivateService() によってデバイスマネージャにサービスのロードが要求されると、デバイスマネージャは、上のコールスタックと同様、ServiceEnum を介して ActivateDeviceEx() を呼び出すのです。

■ホストプロセスを複数起動する
ユーザモードドライバやサービスを割り当てるホストプロセスは、デバイスマネージャ内の Reflector において ID により識別され、必要に応じて(つまり、UserProcGroup で指定された ID のホストプロセスが起動済みでなければ)起動されることを上で述べました。ユーザモードドライバとサービスを割り当てるホストプロセスの ID は、それぞれデフォルト値があり、ユーザモードドライバのホストプロセス(udevice.exe)は 3 で、サービスのホストプロセスは 2 です。このデフォルト値は、
 %_WINCEROOT%/public/common/oak/files/common.reg
で定義されています。common.reg の中にある、PROCGROUP_DRIVER_MSFT_DEFAULT というのが udevice.exe のデフォルト ID で、PROCGROUP_SERVICE_MSFT_DEFAULT が、servicesd.exe のデフォルト ID です。

ユーザモードドライバやサービスに対するレジストリ設定で、デフォルトの ID 以外の値を UserProcGroup に指定すると、デフォルトのものとは別にホストプロセスが生成・起動されます。たとえば、FTP サーバ(ftpd)を組み込んだ OS イメージにおいて、 OS Design のレジストリ設定ファイル(OSDesign.reg)に次の行を追加すると、ftpd 専用の servicesd.exe が起動します。

[HKEY_LOCAL_MACHINE\Services\FTPD]
    "UserProcGroup"=dword:8

[HKEY_LOCAL_MACHINE\Drivers\ProcGroup_0008]
    "ProcName"="servicesd.exe"
    "ProcVolPrefix"="$services"
    "ProcTimeout"=dword:20000


上の例では、ID が 8 で servicesd.exe を実行するホストプロセスを設定して、そのプロセスに ftpd.dll がロードされるように、FTPD の UserProcGroup の値に 8 を指定しています。このようにすると、servicesd.exe のプロセスが二つ起動されて、二番目の方には ftpd.dll だけがロードされて動きます。下の図は、カーネルデバッガの「スレッド」ウィンドウで、ftpd.dll だけがロードされた servicesd.exe を表示した画面です。

ftpd.dll だけをロードした servicesd.exe

ftpd.dll だけをロードした servicesd.exe

■Reflector によるホストプロセスの呼び出し
ところで、カーネルの一部であるデバイスマネージャ内の Reflector から、ホストプロセスのユーザモードドライバやサービスを呼び出す処理は、どうなっているのでしょうか?

アプリケーションからカーネルモードのデバイスドライバを呼び出す場合であれば、DeviceIoControl() の呼び出しによってシステムコールが実行され、カーネルに制御が移ったのちに、カーネル内部でデバイスマネージャからデバイスドライバが呼び出されます。しかし、ユーザモードドライバの場合には、カーネルモードからユーザモードへの遷移が必要です。

カーネル内部からユーザプロセス内の DLL を呼び出す機能は、カーネル内部で実装されており、システムコールと似た仕組みです。カーネルのソースコードでいうと、
 %_WINCEROOT%/private/winceos/coreos/nk/kernel/apicall.c
にある NKHandleCall() の中で呼び出している MDCallUserHAPI() という関数が、カーネルからユーザプロセスを呼び出すためのものです。この MDCallUserHAPI() は、MD (Machine Dependent) という接頭辞の通り、CPU アーキテクチャごとに異なる実装となり、アセンブラで書かれています。WEC 7 の場合ですと、ARM, MIPS, x86 用のソースが、それぞれ次の場所にあります:

 %_WINCEROOT%/private/winceos/coreos/nk/kernel/arm/armtrap.s
 %_WINCEROOT%/private/winceos/coreos/nk/kernel/mips/except.s
 %_WINCEROOT%/private/winceos/coreos/nk/kernel/x86/fault.c (※インラインアセンブラ)

MDCallUserHAPI() を呼び出している NKHandleCall() は、API の実体を呼び出すための関数ですが、API の「ハンドル」がユーザモードに所属している場合は、MDCallUserHAPI() によってカーネルモードからユーザモードへの遷移を伴う呼び出しを行い、それ以外の場合は、カーネル内部での呼び出しを実行します。カーネルモードからユーザモードへの遷移処理の実体は、apicall.c にある SetupCallToUserServer() (および、この関数から呼び出される、各種プロセッサ依存の実装を持つ関数)です。

■WinCE 6.0 以前の仕組み
さて、WinCE 6.0 以前、つまり WinCE 5.0 までは、デバイスドライバは、全てユーザモードで動作していました。

WinCE 5.0 までは、純粋なマイクロカーネル構造であり、デバイスマネージャは、カーネルにロードされる DLL ではなく、マイクロカーネルとは独立したプロセス(device.exe)だったのです。各デバイスドライバは、ユーザプロセスで動作するため、通常のアプリケーションと同様に API を呼び出すことができ、デバイスドライバが直接 GUI 表示を行うことも可能だったようです。そして、ユーザモードで動作するデバイスドライバがハードウェアを直接制御できるように、ユーザモードから物理アドレスを直接アクセスすることが可能になっていました。

WinCE 5.0 から WinCE 6.0 への移行において、この構造に見直しが加えられ、カーネルランドとユーザランドを明確に区別して、ユーザモードから物理アドレスを直接アクセスできないようになったのです。それに合わせて、仮想記憶機構にも大幅な変更が加えられています。WinCE 6.0 において、デバイスマネージャが、カーネルから独立したプロセスではなくカーネルにロードされる DLL となったことに伴い、デバイスドライバは、デフォルトではカーネルモードで動作するようになりました。これは、パフォーマンスの面では有利である一方、システムの堅牢性という観点から見ると、好ましくない面があります。前回のエントリでも述べたように、デバイスドライバのバグによって、カーネル全体が障害を起こしてしまう可能性があるからです。

そのため、WinCE 6.0 では、(WinCE 5.0 までと同様に)ユーザモードでデバイスドライバを動かすことも可能なように、User Mode Driver Framework が導入されたのです。WinCE 5.0 から WinCE 6.0 への移行における、デバイスドライバ回りのアーキテクチャの変更については、WinCE 6.0 のβ版がリリースされた頃に提供されたと思われるドキュメントが Microsoft 社のサイトからダウンロードできますので、そちらをご覧になると、参考になるでしょう。

 Future Directions For The Windows CE Device Driver Architecture
 http://download.microsoft.com/download/5/b/9/5b97017b-e28a-4bae-ba48-174cf47d23cd/WCE030_WH06.ppt

WinCE 5.0 までのデバイスドライバと、WinCE 6.0 以降のユーザモードドライバを比べると、物理アドレスを直接アクセスできるかどうかという点が異なります。また、アプリケーションからデバイスドライバを呼び出す場合のオーバーヘッドを考えると、WinCE 6.0 以降のユーザモードドライバは、Reflector を介するために、WinCE 5.0 までのドライバ呼び出しに加えると、若干オーバーヘッドが大きくなっていると思われます(※一方、WinCE 6.0 のカーネルモードドライバは、WinCE 5.0 までとは異なり、カーネルからデバイスマネージャのプロセスを呼び出すシーケンスがありませんから、上述したように、オーバーヘッドが小さくなっていると考えられます)。

デバイスドライバから GUI 表示を行う場合のことを考えると、WinCE 6.0 以降では、ハードウェアを制御する部分をカーネルモードのドライバ、GUI 表示を行う部分をユーザモードドライバとして、分割しなければならず、WinCE 5.0 までのデバイスドライバに慣れ親しんだ開発者にとっては、不便に思える変更だったことでしょう。

なお、堅牢性と柔軟性の観点から考えると、今回のエントリで説明したように、WinCE 6.0 で導入された User Mode Driver Framework では、個々のユーザモードドライバを、それぞれ異なるホストプロセス(udevice.exe)に割り当てることも可能なため、WinCE 5.0 までのドライバアーキテクチャよりも強力だと言えるんじゃないかと思います。開発途上の、不安定なデバイスドライバは、レジストリ設定で専用のホストプロセスを割り当てて動かし、安定した時点で、他のユーザモードドライバと同じホストプロセスへ移す、といった開発の進め方も可能になっているからです。

■マイクロカーネルの考え方~Android との比較
今回のエントリを終える前に、マイクロカーネルの考え方を採用した他の OS として、Android について書いてみます。皆さんご存じの通り、Android は、カーネルに Linux カーネルを用いて構築された OS であり、マイクロカーネルの OS ではありません。WinCE 5.0 を除いて、現在市場で広く使われている純粋マイクロカーネル構造の OS といえば、前回も述べた QNX があります。

しかし、Android という OS の設計には、マイクロカーネルの考え方を踏襲している部分があると僕は思うのです。おそらくは30代以上の、マニアックな OS を好きな方なら、もしかすると BeOS という OS のことをご存じかも知れません。1990年代の後半、SMP 型のマルチプロセッサ機に対応し、パーソナルコンピュータの分野で普及することを目指した OS が BeOS です。実は、Android の 1.0 が発表された頃、公表されていた Android 開発チームのコアメンバー10数人の半分は、その BeOS に関わっていたエンジニアでした。そのためか、Android の内部には、BeOS に由来する仕組みが残っているようです。

特に、Linux カーネルに独自の改変を加える形で実現されている、”binder” という名前の軽量なプロセス間通信機構および、binder を支える “ashmem” という名前の共有メモリ機構は、BeOS の考え方を踏襲したものだと僕には思えます。Android では、Linux カーネルを採用してはいるものの、ユーザランドは全く独自であり、Linux ディストリビューションと呼べる存在では、ありません。Java API として提供される各種ミドルウェアの内部実装を見ると、Linux カーネルを一種のマイクロカーネルとして使い、各種ミドルウェア内部で動作するサービスモジュール(サービスプロセス)群が、軽量プロセス間通信機構と共有メモリ機構を駆使して連携する仕組みとなっているように思われます。

WinCE 5.0 までのデバイスマネージャのような、デバイスドライバをホストするサービスプロセスこそありませんが、音声や映像の入出力・レンダリング処理を司る media server というサービスプロセスなどは、BeOS のファンだった僕にとって、その世界を彷彿とさせるものです。

Android では、それらのサービスプロセス群を起動・監視する役割を担うプロセスとして init が動作し、あるサービスが障害により動作を停止したり強制終了してしまった場合には、それを再起動する仕組みになっているようです。これは、OS が提供するシステム機能を、複数のサービスプロセスに分割して、堅牢性を高めるという、マイクロカーネル構造の考え方に通じるものだと思います。

Android は、Linux カーネルを採用することで、Linux 用に開発されたデバイスドライバをそのまま流用し、そのうえで、開発チームが慣れ親しんでいた BeOS の構造を踏襲した設計を行った OS ではないかというのが、僕の想像です。

振り返って WEC/WinCE を見てみると、WinCE 5.0 までの純粋マイクロカーネル構造から、UNIX 系 OS に近い(そして、WindowsNT 系統のカーネルにも近い)メモリモデルやデバイスドライバモデルに移行しつつも、純粋マイクロカーネル構造の時からモジュール同士のインタフェースを大幅に変更することなく(※実際、デバイスマネージャは、.exe から .dll に変わったものの、インタフェースと内部の構造には、必要最小限の変更しか加わっていません)、移行前の資産のうち活かせる部分は残して、堅牢性を高める工夫をしたと言えるんじゃないかと思います。そして、前回と今回の二回にわたってとりあげた User Mode Driver Framework の設計は、可用性を高める効用があったと評価できる、と思うのです。

Add comment 2012/12/03 koga

Telnet サーバと udevice.exe(1/2)

このタイトルをご覧になって、「おや?」と首をかしげた方が、いらっしゃるかも知れません。
「サービスの DLL をロードするホストプロセスは、servicesd.exe であって、udevice.exe じゃないはずだけれど。」そう思った方は、%_WINCEROOT%/public/COMMON/oak/files/common.bib をご覧になってみて下さい。何の事だか分からない、というあなたは、WEC/WinCE の User Mode Driver フレームワークについての今回と次回のエントリを、もしかしたら興味深く感じるかも知れません。

それから、「telnetd なら、普通は inetd とか xinted じゃないの?」と思った UNIX なあなたは、いわゆるネットワークデーモンの、WEC/WinCE での実現方式について、今回のエントリが参考になるかも知れません。

■スーパーサーバと servicesd.exe
Linux など UNIX 系の OS では、Telnet サーバ(telneted)などのサーバプログラムは、inetd や xinetd などのスーパーサーバによって起動されるのが一般的です。これに対し、WEC/WinCE では、servicesd.exe というプロセスが、その役割を果たします。servicesd.exe については、WEC/WinCE の次のリファレンスページで説明されています:

 Servicesd.exe (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee499183

 Services.exe Application Development (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee500585(v=winembedded.60)

上の WinCE 6.0 のリファレンスページでは、servicesd.exe ではなく services.exe という名前になっていますが、これは、WinCE 6.0 R2 より前の版でのものです。WEC 7 の方のリファレンスページに書かれているように、WinCE 6.0 R2 からは、services.exe ではなく、servicesd.exe となっています。

servicesd.exe(および、R2 以前の WinCE 6.0 での services.exe)は、レジストリの [HKEY_LOCAL_MACHINE\Services] キー配下のキーで指定されたサーバ群を、[HKEY_LOCAL_MACHINE\Services\<サーバ名>\Accept\<ポート番号>] というキーで指定されたポート番号に対して、クライアンからの接続が起きた際に起動します。

servicesd.exe に対する、ポート番号とサーバ(service DLL)との関連付けについては、リファレンスの次のページで説明されています:

 Registering a Super Service Automatically (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee498484(v=winembedded.60)

実際のレジストリ設定の内容は、
 %_WINCEROOT%/public/servers/oak/files/servers.reg
をご覧下さい。telnetd の他、httpd に対する設定も記載されています。

ところで、UNIX 系 OS の inetd や xinetd とは異なり、WEC/WinCE の servicesd.exe では、各サーバを独立したプロセスとして起動するのではなく、サーバ機能を実装した DLL を呼び出します。WEC/WinCE では、UNIX 系 OS で言うデーモンプログラムは、個々に独立したプロセスによって実行されるアプリケーションではなく、DLL として実装されているのです。より正確には、各サーバは、ユーザモードのデバイスドライバとして実装されており、servicesd.exe が、各サーバの DLL をロードして、IOCTL コードを用いてそれらを制御します。

WEC/WinCE は、組み込み機器向けの OS ですから、より少ないメモリで動作するための方策として、各サーバを個別のプロセスとして動かすのではなく、一つのプロセス(servicesd.exe)にロードされる DLL として実装して動かすようになっているのだと思います(注1)。そのための仕組みとして、ユーザモードのデバイスドライバの枠組みを利用している、というわけです。

注1:次回で述べますが、servicesd.exe は、実は複数起動することができ、サーバごとにプロセスを割り当てて動かすことも可能です。

■ユーザモードのデバイスドライバ
ここで、ユーザモードのデバイスドライバとは、カーネルではなく、ユーザプロセスにロードされて、ユーザ空間で動作するデバイスドライバのことです。WinCE 6.0 以降では、デバイスドライバは、カーネルにロードされてカーネル空間で動作するのが基本となっています。これは、伝統的な UNIX 系 OS と同じです。一方、純粋なマイクロカーネル構造だった WinCE 5.0 までは、独立したプロセスとして動作するデバイスマネージャが、デバイスドライバをロードして動かす仕組みになっていました。WinCE 5.0 から WinCE 6.0 への移行において、カーネルの仮想記憶機構に大幅な変更が加えられ、純粋なマイクロカーネル構造から、UNIX 系 OS に近い構造に変わった際に、デバイスマネージャにも変更が加えられました。つまり、独立して動作するプロセス(device.exe)から、カーネルにロードされる DLL(device.dll)となったのです。

WinCE 5.0 から WinCE 6.0 への移行の際の変更については、以前のエントリ(「レジストリ変更内容の永続化(2/2)」の末尾にある「おまけ」の項)でも紹介しました。

さて、Linx や Mac OS X など、UNIX 系 OS でも、ユーザモードのデバイスドライバに対する取り組みは、あります。プリンタドライバや、SVGAlib、および USB のクラスドライバなどが、典型的な例でしょう。

 Wikipedia 英語版の “Device driver” にある “Kernel-mode vs user-mode” の説明
 http://en.wikipedia.org/wiki/Device_driver#Kernel-mode_vs_user-mode

 Mac OS X の “User-Mode USB Device Arbitration”
 https://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/ClassicUSBDeviceArb.html

ユーザモードのデバイスドライバの利点は、上の Wikipedia のページにも書かれているように、ドライバのバグによってカーネル全体が障害を起こすことを防ぎ、その結果システム全体の安定性を確保しやすくなる、ということだと思います。これは、Linux などの「モノリシックカーネル」に対して、純粋なマイクロカーネル構造の OS の利点としても、よく言われることです。純粋なマイクロカーネル構造の OS として、(WinCE 5.0 以外で)今でも現役で使われている QNX が、その一例です。

また、GUI の表示など、カーネルモードでは呼び出すことのできない API をデバイスドライバから利用したい場合に、デバイスドライバをカーネルモード部分とユーザモード部分に分割して実装し、ユーザモードのデバイスドライバで API を呼び出すようにする、といった方策での利用もあります。そのような場合には、カーネルモードのデバイスドライバとユーザモードのデバイスドライバが連携するための仕組みが必要です。WEC/WinCE では、そのための仕組みとして User Mode Driver フレームワークが提供されています。

■User Mode Driver フレームワーク
User Mode Driver フレームワークは、リファレンスの次のページで説明されています:

 User Mode Driver Framework (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee482294

 User Mode Driver Framework (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee482294(v=WinEmbedded.60).aspx

大まかな構造は、WinCE 6.0 のリファレンスにある、アーキテクチャ図を見ると分かりやすいでしょう:

 User Mode Driver Framework Architecture (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee486510(v=winembedded.60)

ユーザモードのデバイスドライバをロードするホストプロセス(User Mode Driver Host)が、カーネル内部のデバイスマネージャとの間の橋渡しを行う User Mode Driver Reflector とやり取りする様子が、上のページの図に描かれています。

User Mode Driver Reflector は、起動済みの User Mode Driver Host にドライバをロードさせたり、あるいは、新たに User Mode Driver Host を起動して、ドライバをロードさせます。ユーザモードのデバイスドライバは、User Mode Driver Host にロードされた後、DeviceIoConrol() や User Mode Driver フレームワークの関数を使って、カーネルモードのデバイスドライバを呼び出して連携動作します。カーネルモードのデバイスドライバやアプリケーションからの、ユーザモードのデバイスドライバの呼び出しは、デバイスマネージャが User Mode Driver Reflector を使って User Mode Driver Host へ呼び出し内容を転送(forward)して、User Mode Driver Host がドライバを呼び出すことによって処理されます。

UNIX 系 OS と同様、WinCE 6.0 以降では、ユーザモードのデバイスドライバは、物理メモリや周辺機器制御のレジスタなどをアクセスすることは、できません(この点が、WinCE 5.0 からの移行の際に大きく変わったことの一つです)。従って、ハードウェアを制御するためには、カーネルモードのデバイスドライバと連携する必要があります。

WEC 7/WinCE 6.0 に付属しているユーザモードのデバイスドライバとしては、ソフトウェア入力パネル(Software Input Panel; SIP)が、その一例です。また、GPS 中間ドライバ(GPS Indermediate Driver; GPSID)も、ユーザモードのデバイスドライバです。

なお、上で挙げた、User Mode Driver フレームワークの WEC 7 のリファレンスのトップページ、および、WinCE 6.0 のリファレンスの次のページにある、ユーザモードのデバイスドライバのサンプルの紹介は、間違いだと思います:

 User Mode Driver Framework Samples (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee484423(v=winembedded.60)

これらのページには、
 %_WINCEROOT%\public\COMMON\oak\drivers\sdcard\SDClientDrivers\GPS
に、ユーザモードのデバイスドライバのサンプルがあると書かれてします。しかし、このディレクトリにソースコードが収録されているのは、実際にはカーネルモードのドライバである、SDIO の GPS モジュールのドライバ(GPSSDIO.dll)です。

上述した二つのユーザモードのデバイスドライバ(SIP と GPSID)の他に、何があるかは、
 %_WINCEROOT%/public/COMMON/oak/files/common.reg
で Flags キーの値に 0×10 ビット(DEVFLAGS_LOAD_AS_USERPROC)が設定されているドライバを見てみると分かります。また、WEC 7/WinCE 6.0 をカーネルデバッガ経由で動かして、プロセス一覧を表示し、udevice.exe にロードされている DLL の一覧を見るのもよいでしょう。

■servicesd.exe と udevice.exe
上で突然、udevice.exe のことを書きましたが、実は、udevice.exe が User Mode Driver Host なのです。そして、udevice.exe と、WEC/WinCE におけるスーパーサーバである servicesd.exe とは、深い関係があります。冒頭で、次のように書いたことを思い出して下さい:

このタイトルをご覧になって、「おや?」と首をかしげた方が、いらっしゃるかも知れません。
「サービスの DLL をロードするホストプロセスは、servicesd.exe であって、udevice.exe じゃないはずだけれど。」そう思った方は、%_WINCEROOT%/public/COMMON/oak/files/common.bib をご覧になってみて下さい。

common.bib を見ると、次の行があるはずです:

; @CESYSGEN IF CE_MODULES_DEVICE
   device.dll      $(_FLATRELEASEDIR)\device.dll               NK  SHMK
   udevice.exe     $(_FLATRELEASEDIR)\udevice.exe              NK  SHM      ★
   devmgr.dll      $(_FLATRELEASEDIR)\devmgr.dll               NK  SHMK
   regenum.dll     $(_FLATRELEASEDIR)\regenum.dll              NK  SHK
   busenum.dll     $(_FLATRELEASEDIR)\busenum.dll              NK  SHK
; @CESYSGEN IF DEVICE_PMIF
   pm.dll       $(_FLATRELEASEDIR)\pm.dll                      NK  SHMK
; @CESYSGEN ENDIF
; @CESYSGEN IF CE_MODULES_SERVICES
   servicesEnum.dll   $(_FLATRELEASEDIR)\servicesEnum.dll     NK  SHK
; Note - servicesd.exe is just renamed udevice.exe.  Services specific functionality via servicesFilter.dll
   servicesd.exe      $(_FLATRELEASEDIR)\udevice.exe          NK  SH        ★★
   servicesFilter.dll $(_FLATRELEASEDIR)\servicesFilter.dll   NK  SH
   services.exe       $(_FLATRELEASEDIR)\services.exe         NK  S
   servicesStart.exe  $(_FLATRELEASEDIR)\servicesstart.exe    NK  SH
; @CESYSGEN ENDIF


上で★★を付けた行を見ると、udevice.exe を servicesd.exe という名前で OS イメージへ格納する設定になっていることが分かります。★の行は、udevice.exe を OS イメージへ格納する設定の行です。つまり、udevice.exe と servicesd.exe の実体は、同じものなのです。

udevice.exe(そして、servicesd.exe も)のソースコードは、
 %_WINCEROOT%/private/winceos/COREOS/device/udevice/
に収録されています。カーネルデバッガを使ってソースコードを追ってみると、servicesd.exe として動作する場合、つまり、telned などのサーバ機能を実装した DLL のホストプロセスとして動作する場合には、スーパーサーバとして必要な動作を行うことが分かります。つまり、WSAStartup() を呼び出して winsock を初期化し、その後、DLL に対するレジストリで設定されたポートで接続待ちするスレッドを起動します。

udevice.exe と servicesd.exe は、実装上共通する部分が多いため、単一のプログラムとして実装し、OS イメージには異なる名前で配置する、という選択がなされたのでしょう。両者が同じ実体であるということは、リファレンスには明記されていませんから、もしかすると、将来は、異なるプログラムとして実装し直される可能性もあります。なお、servicesd.exe のスーパーサーバとしての機能は、
 %_WINCEROOT%/private/winceos/COREOS/device/udevice/
ではなく、
 %_WINCEROOT%/private/winceos/COREOS/device/services/filter/
に収録されたソースコードで実装されています。これは、udevice.exe(つまり、servicesd.exe)がリンクしている servicesFilter.dll という DLL のソースコードです。

Add comment 2012/08/27 koga

CeLogFlush.exe と Kernel Tracker

WEC 7 や WinCE 6.0 のデバイスドライバや、アプリケーションの開発を行っていて、期待しているパフォーマンスが達成されなかったり、予想していたタイミングでスレッドが実行されない、などの問題が生じた場合、どこに要因があるのかを調べるには、カーネルのログ機能と Kernel Tracker を使うのが便利です。WEC/WinCE カーネルのログ機能は、デバッグメッセージを出力するなどの単純なものではなく、スレッドの切り替わりや、各種同期オブジェクトの獲得や解放、および、割り込み処理の発火など、OS 内部の詳細な動きを記録できるようになっています。

WEC/WinCE カーネルが出力したログの内容は、Kernel Tracker を使って、グラフィカルに表示できます。以下に、WEC 7 をデバイスエミュレータで動かして取得したログの内容を、Kernel Tracker で表示した様子を示します。

Kernel Tracker の画面キャプチャ

Kernel Tracker の画面キャプチャ

上の画面キャプチャを見ると、標準シェル(explorer.exe)のスレッドが 100ms の Quantum を使い果たし、servicesd.exe 内の同じ優先度を持つスレッドに切り替わった様子が分かります。また、ほぼ一定間隔で割り込み応答動作が起きていることも分かります。これは、1ms おきに発生するタイマ割り込みに対するものです。

このように、WEC/WinCE カーネルのログ機能を使うと、1ms より細かい時間精度でスレッドの動きを知ることができ、OS 内部の動作を分析するのに役立ちます。カーネルデバッガを使ってステップ実行する場合とは異なり、通常の動作での OS 内部の振る舞いを知ることができますので、うまく使えば、強力なツールとなります。また、後述するように、出荷後のデバイスに搭載されているものなど、ログ機能を有効にしない OS イメージに対しても、一時的にログ機能を有効にしてログ採取できるのも、非常に便利な点でしょう。

■CeLogFlush.exe によるログ採取(標準設定)
WEC/WinCE カーネルのログ機能については、リファレンスの次のページで説明されています:

 CeLog Event Tracking (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee479601

 CeLog Event Tracking Overview (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee480432(v=winembedded.60)

このログ機能を使って、WEC/WinCE カーネルが出力するログをファイルに保存する手順は、次の通りです。以下の説明は WEC 7 の場合ですが、WinCE 6.0 でも同様です。

  1. OS Design のカタログ項目で、Target Control Support (Shell.exe) を選択する(SYSGEN_SHELL を有効にする)。
  2. OS Design の「構成プロパティ」の Build Options で、Enable KITL を Yes に設定する。
  3. 同じく Build Options で、Flush tracked events to release directory を Yes (IMGAUTOFLUSH=1) に設定する。

ただし、WinCE 6.0 の場合は、次のいずれかの手順が追加で必要です。

  • OS Design の「構成プロパティ」の Build Options で、Enable event tracking during boot を Yes (IMGCELOGENABLE=1) に設定する。
  • project.bib の FILES セクションに、celog.dll を追加する。

つまり、WinCE 6.0 の場合は、IMGAUTOFLUSH に加えて IMGCELOGENABLE も1に設定しなければ(または、明示的に celog.dll を .bib ファイルに記述しなければ)、カーネルのログ出力が起きません。一方、WEC 7 の方は、IMGCELOGENABLE は設定しなくても、IMGAUTOFLUSH を1に設定すればカーネルのログ出力が起きるのです。実際、リファレンスを見ても、そのように説明されています:

 Including CeLogFlush in a Run-Time Image (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-us/library/ee481413(v=WinEmbedded.60).aspx

 Enable CeLog Event Tracking (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee480716

WinCE 6.0 と WEC 7 の、この違いは、%_WINCEROOT%/public/COMMON/oak/files/common.bib の記述内容の違いによるものです。WinCE 6.0 の common.bib には、IMGCELOGENABLE が1の場合に celog.dll を MODULES セクションに追加する行があるだけです。一方、WEC 7 の common.bib では、次のようになっています。

MODULES
;  Name            Path                                           Memory Type
;  --------------  ---------------------------------------------  -----------
…
IF IMGCELOGENABLE
   celog.dll       $(_FLATRELEASEDIR)\celog.dll                NK  SHK
ENDIF IMGCELOGENABLE
…

; ====================================================================
; FILES section
;
; ====================================================================

FILES
…
; Setting IMGAUTOFLUSH or IMGOSCAPTURE without IMGCELOGENABLE will include
; celog.dll in the FILES section instead of MODULES, so that the DLL will
; be loaded late enough to read settings from the device registry.
IF IMGCELOGENABLE !
IF IMGAUTOFLUSH
   celog.dll       $(_FLATRELEASEDIR)\celog.dll                NK  SH
ENDIF IMGAUTOFLUSH
IF IMGOSCAPTURE
   celog.dll       $(_FLATRELEASEDIR)\celog.dll                NK  SH
ENDIF IMGOSCAPTURE
ENDIF IMGCELOGENABLE


WEC 7 の場合は、common.bib の内容が上のようになっているため、IMGCELOGENABLE を設定せずに IMGAUTOFLUSH だけを1に設定した場合は、celog.dll が .bib ファイルの FILES セクションに追加されますので、OS イメージに celog.dll が収録されます。これに対して、WinCE 6.0 では、IMGCELOGENABLE が1に設定されなければ celog.dll が OS イメージに収録されません。そのため、IMGCELOGENABLE も1に設定するか、または、project.bib の FILES セクションに celog.dll の行を追加する必要があるのです。

上記の設定を行った OS Design をビルドして、ターゲットデバイスにダウンロードして動かすと、Flat Release Drectory、つまり環境変数 _FLATRELEASEDIR が指すディレクトリに celog.clg というファイルが作られて、カーネルが出力したログが書き込まれます。ターゲットデバイスと Platform Builder との接続を切れば、celog.clg ファイルを開くことができるようになりますので、Kernel Tracker で開いてみて下さい。

WEC 7 の場合は、.clg ファイルが Kernel Tracker と関連づけられているため、.clg ファイルのアイコンをダブルクリックすると、Kernel Tracker が起動します。WinCE 6.0 の場合は、Kernel Tracker を起動した後、File メニューで .clg ファイルを開く必要があります。詳細は、次のページをご覧下さい:

 Remote Timeline Viewer (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/gg156030

 Starting Kernel Tracker in File Mode (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee480674(v=winembedded.60)

■CeLogFlush.exe によるログ採取(設定のカスタマイズ)
さて、KITL と Target Control Support (Shell.exe) を有効にした OS イメージで、Flat Release Directory に WEC/WinCE カーネルのログを出力できることは分かりました(Target Control Support (Shell.exe) ではなく、Release Directory File System を有効にするのでも構いません)。これらを有効にしない限り、カーネルのログをファイルへ出力することは、できないのでしょうか?

Flat Release Directory 以外のディレクトリへファイルを出力することは、レジストリ設定により可能です。WEC/WinCE カーネルのログは、celog.dll によって RAM 上のリングバッファへ格納され、その内容を、CeLogFlush.exe がファイルへ出力する、という仕組みになっています。CeLogFlush.exe および celog.dll に対するレジストリ設定は、リファレンスの次のページで説明されています。

 CeLog Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee480468

 CeLogFlush Registry Settings (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee481192(v=winembedded.60)

ログを出力するファイルは、上のページの説明にある通り、[HKEY_LOCAL_MACHINE\System\CeLog] キーの FileName で設定できます。たとえば、SD カードにログファイルを出力する場合は、次のような行を .reg ファイルへ追加すればよいでしょう。

[HKEY_LOCAL_MACHINE\System\CeLog]
    "BufferSize"=dword:20000
    "FileName"="\\Storage Card\\celog.clg"
    "FileSize"=dword:0
    "FileFlags"=dword:0
    "FlushTimeout"=dword:2710
    "ThreadPriority"=dword:F8
    "Transport"="LocalFile"
    "UseUI"=dword:0
    "ZoneCE"=dword:c003e2


なお、BufferSize や FlushTimeout の値は、デフォルトよりも小さくしないで下さい。これらを小さくすると、celog.dll が確保するリングバッファがすぐに満杯になったり、あるいは、ログが出力されていなくてもすぐにバッファのフラッシュ時間に達してしまい、必要以上にログ出力が増えます(CeLogFlush.exe によるファイルへのログ書き込みによっても、カーネルのログ出力が発生することに留意して下さい)。その結果、OS の実効速度が実用にならないものになってしまう場合もあるからです。

もう一点注意です。[HKEY_LOCAL_MACHINE\System\CeLog] キーの FileName 値の設定で、ログの出力先を外部ストレージにする場合、そのストレージデバイスがマウントされるよりも前に CeLogFlush.exe が起動されないようにしなければいけません。IMGAUTOFLUSH を1に設定した場合、common.reg ファイルの記述により、[HKEY_LOCAL_MACHINE\init] キーに対する以下の設定が追加されます:

[HKEY_LOCAL_MACHINE\init]
        "Launch05"="CeLogFlush.exe"


この設定では、デバイスマネージャ(device.dll)よりも先に CeLogFlush.exe がロード・起動されてしまうため、CeLogFlush.exe が初期化時に出力ファイルを作成できず、エラーで終了してしまいます。従って、device.dll の起動順序(Launch20)よりも後の起動順序を設定する必要があります。デバイスマネージャが起動してストレージがマウントされるまでに若干の時間がかかることを考慮すると、gwes.dll(Launch30)の後にする方が、より確実だと思われます。

■ログ機能を有効にしていない OS イメージでのログ採取
ここまでの説明で、celog.dll と CeLogFlush.exe を OS イメージに組み込んでいれば、WEC/WinCE カーネルのログをファイルへ出力できることが分かりました。しかし、カーネルのログが常にファイルへ出力されると、若干のオーバーヘッドを生みますし、また、セキュリティの観点からも、好ましいことでは、ありません。このことは、上で紹介した WEC 7 のリファレンスページにも注意書き(Note)として記載されています。

さて、今回のエントリの冒頭で、次のように書きました:

また、後述するように、出荷後のデバイスに搭載されているものなど、ログ機能を有効にしない OS イメージに対しても、一時的にログ機能を有効にしてログ採取できるのも、非常に便利な点でしょう。

実は、celog.dll と CeLogFlush.exe を OS イメージに組み込んでいなくても、OS の起動後に celog.dll をカーネルにロードさせて、ログ機能を有効にできるのです。従って、USB メモリなどの外部ストレージに celog.dll と CeLogFlush.exe を入れておき、OS の起動後に、手動で CeLogFlush.exe を始動すれば、必要な時にだけログを採取できます。このことは、リファレンスの次のページでも説明されています:

 Collecting Data On A Standalone Device With CeLogFlush (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee480886(v=winembedded.60)

CeLogFlush.exe は、起動すると終了要求されるまで、celog.dll が出力したログをリングバッファから読み出してファイルへ出力するループ動作を実行します。”SYSTEM/CeLogFlush Quit” という名前の event に対して SetEvent() を呼び出すことにより、CeLogFlush.exe に終了要求できます。または、CeLogFlush.exe のソースファイルと同じ場所に収録されている、CeLogStopFlush というサンプルプログラムも参考になります。CeLogStopFlush は、CeLogFlushCommon.lib というライブラリを CeLogFlush.exe と共用しており、CeLogFlushCommon.lib の FLUSH_SignalStopFlush() という関数を呼び出します。FLUSH_SignalStopFlush() は、”SYSTEM/CeLogFlush Quit” という名前の event を OpenEvent() でオープンして、SetEvent() を呼び出すだけです(※SetEvent() の後、CloseHandle() を呼び出します)。

■おまけ
いかがでしょうか?ここまでの説明で、WEC/WinCE カーネルのログ機能が、出荷用の OS イメージに対する障害解析ツールとしても利用可能な、強力なものであることを、お分かり頂けたのではないかと思います。以下に、このログ機能について、いくつか補足します。

(1)CeLogFlush.exe と celog.dll のソースコード
 CeLogFlush.exe と celog.dll のソースコードは、それぞれ次の場所にあります:

 CeLogFlush.exe
 %_WINCEROOT%/public/COMMON/sdk/samples/celog/flush/CeLogFlush/

 celog.dll
 %_WINCEROOT%/private/winceos/COREOS/nk/celog/celogdll/

 CeLogFlush.exe と celog.dll は、”SYSTEM/CeLog Data” という名前の共有メモリ領域(CreateFileMapping() によって生成されるメモリマップドオブジェクト)上に配置したリングバッファを介して、WEC/WinCE カーネルが出力したログデータを受け渡しします。リングバッファが満杯に近付くと、celog.dll が event オブジェクトを使って CeLogFlush.exe に通知して、リングバッファの内容をファイルへ出力させる、という連携です。リングバッファのサイズは、celog.dll が共有メモリ領域を生成する際に、レジストリの設定値に従って決定します。celog.dll は、共有メモリ上のリングバッファ構造体のヘッダにバッファサイズを書き込むことにより、CeLogFlush.exe にサイズを伝えます。CeLogFlush.exe は、リングバッファが満杯に近付かなくとも(つまり、celog.dll から event オブジェクトで通知されなくとも)、レジストリで設定されたタイムアウト時間が経過すると、リングバッファの内容をファイルへ出力します。従って、ログ出力の量が少ない場合でも、一定周期でログファイルへの書き出しが行われます。

(2)”Enable event tracking during boot” を設定しない場合の動作
 OS Design の「構成プロパティ」の Build Options で、”Enable event tracking during boot” を Yes (IMGCELOGENABLE=1) に設定しない場合とする場合の違いについて、もう少し詳しく述べておきます。

  IMGCELOGENABLE を1に設定した場合としない場合の違いは、WEC/WinCE カーネルの初期化が完了した後から、カーネルの起動が完了するまでの間のログ出力を採取するかどうかです。つまり、IMGCELOGENABLE を1に設定した場合は、CeLogFlush.exe が起動する前に出力されたログも採取できるのに対し、IMGCELOGENABLE を1に設定しない場合は、CeLogFlush.exe が起動した以降のログのみ採取可能となります。この違いは、celog.dll がロードされるタイミングの違いによって生じます。そのタイミングの違いを引き起こすのは、.bib ファイルの設定です。

  WEC 7 の場合、common.bib の設定で、IMGAUTOFLUSH のみ1に設定した場合(IMGCELOGENABLE は1に設定しない場合)は、celog.dll が .bib ファイルの MODULES セクションではなく、FILES セクションに配置されると述べました。この結果、WEC/WinCE カーネルの初期化が完了した直後のタイミングでは、celog.dll がロードされず、CeLogFlush.exe によって初めてロードされるのです。これについて、もう少しだけ詳しく述べます。

  WEC/WinCE カーネルの初期化が終わり、マルチスレッドモードへ遷移して最初に起動されるスレッドが実行する関数である SystemStartupFunc() の中で、LoggerInit() という、ログ機能の初期化関数を呼び出します。この LoggerInit() は、”CeLog.dll” を引数として LoadKernelLibrary() を呼び出し、celog.dll をカーネルにロードすることを要求します。しかし、この時点では、filesys.dll がロードされておらず、ファイルシステム機能が初期化されていません。そのため、カーネルの loader は、OS イメージの中の modules、つまり、.bib ファイルの MODULES セクションに配置された DLL しかロードできないのです(※ちなみに、SystemStartupFunc() は、LoggerInit() を呼び出す前に、LoaderInit() を呼び出してカーネルの loader を初期化します)。そのため、LoggerInit() による CeLog.dll のロードは成功せず、その時点では、ログ出力が有効になりません。その後、CeLogFlush.exe が起動すると、CeLogFlush.exe の初期化処理において、再び “CeLog.dll” を引数として LoadKernelLibrary() が呼び出されます。この時点では、filesys.dll が動作していますので、FILES セクションに配置された DLL もロードできる、というわけです。

  さて、celog.dll は、ロードされると、InitLibrary() という初期化関数を呼び出します。この関数の中で、IOCTL_CELOG_REGISTER を ioctl コードとする KernelLibIoControl() 呼び出しを行い、ログ出力関数群の関数テーブル(CeLogExportTable 構造体)をカーネル本体に登録します。これらの関数は、カーネル本体の中にあるログ出力部に登録されます。カーネル本体の中にあるログ出力部は、登録された関数テーブルの中の pfnCeLogQueryZones というメンバを使って、ログ出力 DLL から、出力対象とする zone の組み合わせを示すマスクビット列を得て、ログ出力の有無判定に使う、という仕組みになっています。

  上で述べた、カーネル本体の処理のソースコードは、それぞれ次の場所にあります:

  SystemStartupFunc()
  %_WINCEROOT%/private/winceos/COREOS/nk/celog/schedule.c

  カーネル本体の中のログ出力部
  %_WINCEROOT%/private/winceos/COREOS/nk/logger/logger.c

(3)Readlog
  Kernel Tracker を使うと、WEC/WinCE カーネルのログ機能が出力したログデータをグラフィカルに表示できることを、冒頭で紹介しました。このログデータ、つまり .clg ファイルの内容を解析する付属のツールは、他にもあります。それが Readlog です。Readlog について、リファレンスの次のページをご覧下さい:

  Readlog Viewing Tool (Windows Embedded Compact 7)
  http://msdn.microsoft.com/en-us/library/ee481220.aspx

  Readlog Viewing Tool (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-US/library/ee481220(v=winembedded.60)

  スレッドの切り替わりの様子などを直観的に見るには、Kernel Tracker が便利ですが、Kernel Tracker には表示されないログ内容をチェックしたり、ログ全体の分析結果などを手早く表示するには、Readlog の方が便利です。ちなみに、WinCE 6.0 の Kernel Tracker では、”Thread Migrate” のログを表示できませんが、WEC 7 のものでは表示できます。この “Thread Migrate” は、システムコールの発生、つまり、プロセス(カーネルである nk.exe 以外のプロセス)中のスレッドの、カーネル呼び出しによる、ユーザモードとカーネルモードの間の遷移に伴うコンテキストスイッチを示します。

(4)ログ機能のカスタマイズ
  ここまでの説明では、WEC/WinCE カーネルのログ出力は、celog.dll によって実行されると述べました。しかし、celog.dll を使わず、独自のログ出力 DLL を実装して使うことが可能です。また、独自のログ出力 DLL を celog.dll と共存させることも可能です(複数のログ出力 DLL を共存させることに意味があるかどうかは、別としてですが)。

  独自のログ出力 DLL を実装する方法について、リファレンスの次のページで説明されています。今のところ、WinCE 6.0 向けのものしかありませんが、WEC 7 でも同様の筈です。興味のある方は、カーネル本体のログ出力部や celog.dll のソースコードと併せ、ご覧になってみて下さい。

  CeLog Tool Customization (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-us/library/ee480013(v=winembedded.60)

  Implementing a Custom Event Tracking Library (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-US/library/ee480279(v=winembedded.60)

  Implementing an Event Tracking Library (Windows Embedded CE 6.0)
  http://msdn.microsoft.com/en-US/library/ee480272(v=winembedded.60)

Add comment 2012/08/16 koga

Previous Posts


Categories

Links

Posts by Authors

Recent Posts

Calendar

2017年3月
« 9月    
 1234
567891011
12131415161718
19202122232425
262728293031  

Posts by Month

Posts by Category

Meta