古賀 信哉のWindows CE Blog

MCP Logo MVP Logo


WEC 7 付属の gdiplus.dll

■WinCE/WEC と GDI+
Windows XP で導入された 2D 描画用の API である GDI+ は、WinCE では利用できません。しかし、Windows Mobile では、gdiplus.dll という DLL が組み込まれており、この DLL により、GDI+ の API を利用できたようです。

たとえば、MSDN フォーラムの “Visual Basic and C# Projects” に投稿された “How to use GDI+ on Windows Mobile?” という質問に対して、Windows Mobile において非公式 API ではあるものの、GDI+ を利用できるというコメントが寄せられています:

 How to use GDI+ on Windows Mobile?
 http://social.msdn.microsoft.com/forums/en-US/vssmartdevicesvbcs/thread/e01dfedb-9b79-468b-9e89-44f59952b2d8

そのコメントでも紹介されている次のページは、GDI+ の Flat API を .NET CF アプリケーションから使うための wrapper ライブラリを提供しています。

 Using GDI+ on Windows Mobile
 http://community.opennetcf.com/articles/cf/archive/2007/10/31/using-gdi-on-windows-mobile.aspx

御存知の方は多いと思いますが、フル版の .NET Framework では、描画 API の実装に GDI+ が用いられています。たとえば、System.Drawing.Graphics クラスの DrawArc() や DrawBezier() などです。しかし、.NET Compact Framework では、System.Drawing.Graphics クラスには DrawArc() や DrawBezier() メソッドが存在しません。上記の wrapper ライブラリは、GDI+ の Flat API を P/Invoke によりマネージドコードから呼び出すためものです。

上のページでは、gdiplus.dll が提供している GDI+ API は、デスクトップ版のサブセットであり、「未実装」エラーを返す関数が多くあると述べています。しかし、サブセットであっても、GDI の API だけでは不足する 2D 描画機能を利用したいと思うことは、少なくありません。gdiplus.dll を WinCE でも使いたいと思う人は多いようで、’gdiplus.dll WinCE’ などで検索すると、「WinCE には gdiplus.dll が付属していないのか?」という質問のページが多く見つかります。

実は、WEC 7 には、gdiplus.dll が付属しています。

■WEC 7 の gdiex/ ディレクトリ
冒頭でも述べたように、WinCE には gdiplus.dll が付属しておらず、GDI+ の API を利用できません。しかし、WEC 7 では、
 %_WINCEROOT%/public/gdiex/
ディレクトリ配下に gdiplus.dll と gdiplus.lib が収録されているのです。このディレクトリは、WinCE 6.0 にも存在しますが、配下に収録されているのは、Imaging API のヘッダファイルとライブラリファイル(.lib)だけです。

gdiplus.dll に関する説明は、WEC 7 のリファレンスには存在しません。Windows Mobile でそうだったように、非公式な API という位置づけのようです。SKU との対応も不明ですから、もし、gdiplus.dll を組み込んだ OS イメージを製品に搭載することを検討される場合には、ランタイムライセンスの販売代理店経由で Microsoft 社に確認する方が良いでしょう。

WEC 7 に gdiplus.dll が付属していることの理由は不明ですが、もしかすると、一部の開発者からの強い要望を受けて、Windows Mobile 向けに提供していたものを WEC 7 にも収録した、ということなのかも知れません。いずれにせよ、便利な機能が付属しているわけですから、もしライセンス上も支障なければ、利用できると嬉しいところです。

■ベジェ曲線描画の動作確認
というわけで、WEC 7 付属の gdiplus.dll を使った描画について、簡単なテストコードを書いて試してみました。制御点が二つだけの単純なベジェ曲線の描画ですが、Flat API を C/C++ から呼び出すコードを書いて動かしてみたところ、問題なく動作しました。ただし、パフォーマンスは評価・実測していません。

以下に、テストコードの関連部分を引用します:

static void
DrawWithGdiPlus(HDC hdc)
{
#define MAKE_ARGB(a, r, g, b) \
    (((ARGB)(b) << 0) \
    |((ARGB)(g) << 8 ) \
    |((ARGB)(r) << 16) \
    |((ARGB)(a) << 24)) \

    GpGraphics*    graphics = NULL;
    GpPath*    path = NULL;
    GpPen*    pen  = NULL;
    PointF    points[] = {
        PointF(0.0f + 50, 0.0f + 50),
        PointF(40.0f + 50, 20.0f + 50),
        PointF(80.0f + 50, 150.0f + 50),
        PointF(100.0f + 50, 10.0f + 50),
    };

    if (0 != ::GdipCreateFromHDC(hdc, &graphics)) {
        return;
    }
    if (0 != ::GdipCreatePath(FillModeAlternate, &path)) {
        goto finish;
    }
    if (0 != ::GdipAddPathBeziers(path, points, 4)) {
        goto finish;
    }
    if (0 != ::GdipCreatePen1(MAKE_ARGB(255, 255, 0, 0), 1.0, UnitPixel, &pen)) {
        goto finish;
    }
    if (0 != ::GdipDrawPath(graphics, pen, path)) {
        goto finish;
    }

finish:
    if (NULL != pen) {
        (void)::GdipDeletePen(pen);
    }
    if (NULL != path) {
        (void)::GdipDeletePath(path);
    }
    (void)::GdipDeleteGraphics(graphics);
}


上記のテストコードを動かした画面キャプチャが、次の図です。

bezier-spline

なお、上の画面キャプチャは、2012/7/4 に書いたエントリ(「デバイスエミュレータで WEC 7 を動かす」)で紹介した、デバイスエミュレータに移植した WEC 7 でテストコードを動かした様子です。興味のある方は、ご自分でテストコードを書いて試してみて下さい。

上のテストコードについて、二点補足しておきます。

  1. GdiPlus.h のインクルードが必要。さらに、上記のコードの場合、Gdiplus と Gdiplus::DllExports に対する using ディレクティブが必要。
  2. GdiPlus.h を始め、GDI+ のヘッダファイルは WEC 7 に付属しないので、別途入手が必要。
  3. アプリケーションの初期化処理と終了処理で、それぞれ、GDI+ の初期化関数と解放関数を呼び出さなければならない。

上記のテストコードのビルドと動作確認を行う際、GDI+ のヘッダファイルは、次のページで公開されているライブラリ(LibGdiplus)に収録されているものを使いました。

 GDI+ for Windows Mobile
 http://www.ernzo.com/LibGdiplus.aspx

このライブラリは、Windows Mobile に収録されている gdiplus.dll 用の wrapper です。冒頭で紹介した wrapper ライブラリとは異なり、マネージドコードではなく、C/C++ 用の wrapper です。

GDI+ の初期化関数と解放関数は、GdiplusStartup() と GdiplusShutdown() です。詳細は、GDI+ のリファレンスをご覧下さい:

 GDI+ Reference > Functions
 http://msdn.microsoft.com/en-us/library/windows/desktop/ms534055(v=vs.85).aspx

GDI+ の Flat API のリファレンスは、次のページに記載されています。

 GDI+ Flat API
 http://msdn.microsoft.com/en-us/library/windows/desktop/ms533969(v=vs.85).aspx

■GDI+ の代替ライブラリ
WEC 7 には gdiplus.dll が付属しており、それを OS イメージに組み込むことで GDI+ の API(のサブセット)を利用できることを、上で紹介しました。では、WinCE 6.0 で GDI+ の API を利用したい場合には、何か方法はないのでしょうか?

簡単な代替策は、ありません。前の節で紹介した LibGdiplus のページ("GDI+ for Windows Mobile")では、Mono プロジェクトの LibGdiplus を紹介していますが、(その紹介でも述べられているように)そのまま使うことは、できません。Mono プロジェクトの LibGdiplus は、X Window System に依存した実装だからです:

 Libgdiplus
 http://www.mono-project.com/Libgdiplus

Mono プロジェクトの Libgdiplus は、Cairo という 2D 描画ライブラリを利用しているようですが、WinCE 6.0 で使えるようにするためには、X Window System に依存した部分の実装を、WinCE へ移植する必要があるでしょう。その移植作業は、移植対象の API 関数を一部に絞ったとしても、それなりに大きな手間だと思われます。

なお、GDI+ が提供する 2D 描画 API のうち、ベジェ曲線(Bezie-spline 曲線)の描画だけであれば、自前で実装したサンプルコードが CodeProject サイトの次のページで公開されています。

 Drawing Qubic Bezier-splines on Pocket PC
 http://www.codeproject.com/Articles/9862/Drawing-Qubic-Bezier-splines-on-Pocket-PC

このサンプルコードの動作確認は行っていませんが、おそらく、WinCE/WEC でも問題なく使えるんじゃないかと思います。

■まとめ
今回は、WEC 7 に付属している GDI+ の DLL(gdiplus.dll)について紹介し、単純なベジェ曲線であれば、問題なく動作することを述べました。gdiplus.dll が、先日発表のあった WEC の次のバージョン(Windows Embedded Compact 2013)でも提供されるのかどうかは、現時点では不明です。しかし、WEC 7 に限定すれば、GDI だけでは不足する 2D 描画機能を実現するために gdiplus.dll の利用を検討することは、価値があるかも知れません。

なお、OS やミドルウェアベンダーの近年の動きを見ていると、高機能な 2D 描画は、3D 描画のグラフィクス機能に統合されるのが一般的な方向性のように思われます。Windows の場合でも、GDI/GDI+ を置き換えていくものとして Windows 7 で導入された Direct2D は、Direct3D の上に実装されているそうです:

 Introducing Direct2D
 http://msdn.microsoft.com/en-us/magazine/dd861344.aspx

今後、Windows Embedded Compact におけるグラフィクス機能が、どのように改良ないしは変革されていくのか分かりません。WEC 7 において gdiplus.dll が追加されたことは、もしかすると、Windows Embedded Compact の歴史における、一過性のものだとして捉えておく方が確実かも知れません。

とはいえ、WEC 7 は、2011年のリリースから7年後の、2018年までは、無償サポートが継続する予定です。従って、開発のターゲットを WEC 7 に絞れば、その他の API やミドルウェアと同様に、gdiplus.dll はアプリケーション開発の有力なツールとなり得るでしょう。この点は、WinCE/WEC の Long Term Suppport の利点だと思います。

Add comment 2013/02/05 koga

「クラウドサービスとスマートデバイスの時代における組み込み開発セミナー」

再来週の月曜日(2/4)、札幌市厚別区下野幌(新札幌)にあるテクノパーク内の、札幌市エレクトロニクスセンターで、国内の Windows Embedded MVP 有志を講師に招いたセミナーが開催されます。主催は、さっぽろ産業振興財団です。

以下に、開催告知の案内文を引用します。興味のある方は、是非ご参加下さいませ :-)

————————————ここから————————————
================================================
     クラウドサービスとスマートデバイスの時代における
          組み込み開発セミナーのご案内

      ~ Windows Embeddedの専門家が札幌に集結 ~
================================================

日時:2013年2月4日(月) 14:00~17:00

場所:札幌市エレクトロニクスセンター 2階 会議室
     札幌市厚別区下野幌テクノパーク1丁目1-10
       アクセス

主催:(財)さっぽろ産業振興財団

受講料:無料 (定員:30名 先着順)

対象者:組込み機器向けのソフトウェア開発やサービス開発をする技術者、
    組込み関連分野のマネージャーなど
内容:

 講演会 14:00 ~ 16:00
  (株)アキタ電子システムズ 伊藤 優 氏 (Windows Embedded MVP)
   「WCFによるWindows ServerとWindows Embeddedの連携」

  (株)コムラッド 高根 英哉 氏 (Windows Embedded MVP)
   「スマートデバイスのコネクティビティを支える無線プロトコルスタック」
   (※調整中)

  (株)カスペルスキー 松岡 正人 氏
   「スマートデバイス時代のデバイス開発とセキュリティ」

 休憩

 パネルディスカッション 16:10 ~ 17:00

  日本でも有数の著名な組込みWindows開発の専門家がパネラーとなって、 「クラウドサービスとスマートデバイスの時代における組み込み開発」について、現在の課題と取り組み、将来の展望を語る、札幌では めったに聞けにない貴重なチャンスです。
  パネラー諸氏に対する質問も受け付けますので、組込み開発の全般、特に Microsoft Windows Embeddedベースでのシステム開発に関する課題や質問が有れば、ぜひご参加下さい。

 パネラー
   講演者3名 (伊藤 優 氏、高根 英哉 氏、松岡 正人 氏)
   岡谷エレクトロニクス(株) 高橋 一夫 氏
   (株)サムシングプレシャス 古賀 信哉 氏

お問い合わせ、お申し込み

 (財)さっぽろ産業振興財団 電話:011-807-6000

 参加をご希望の方は、メールの件名に「組み込み開発セミナーの参加」
と記し、所属の組織名、氏名(ふりがな)、連絡先の電話番号を、
「it-pro@sec.or.jp」まで、メールで連絡してください。
————————————ここまで————————————

3 comments 2013/01/25 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

IME と、ソフトウェアキーボードに関する tips

■はじめに
ご存じの方も多いと思いますが、WEC/WinCE には、日本語入力の IME (Input Method Editor) が付属しています。WEC 7 ですと、Platform Builder の Catalog Items View で次のカタログ項目を選択して OS Design をビルドすることにより、日本語 IME 3.1 が OS イメージに組み込まれます。

 Core OS
  International
   Language
    Japanese
     Input Method Editor (Choose 1)
★     IME 3.1

日本語入力用の IME として、IME 3.1 に加えて Pocket IME 2.0 も付属しており、Catalog Items View には、IME 3.1 と同じ階層に表示されます。今回は、Pocket IME ではなく、IME についての tips を紹介します。

■IME の初期入力モードの設定
IME の入力モードは、IME ツールバーの入力モードアイコンをクリックして表示されるポップアップメニューで変更できます。これは、WinXP/Vista/Windows 7 と同様です。ただし、WEC/WinCE 付属の IME では、プロパティダイアログで初期入力モードを設定することができません。初期入力モードは、レジストリでのみ設定可能なようです。

IME のレジストリ設定項目は、リファレンスの次のページで説明されています:

 Japanese IME 3.1 Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee491573.aspx

 Japanese IME 3.1 Registry Settings (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee491573(v=winembedded.60).aspx

なお、WinCE 6.0 の方のリファレンスには、”Japanese IME 3.1 Architecture” というページや、”Japanese IME 3.1 Customizable User Interface” というセクションがあり、IME のカスタマイズを行う際に必要となる情報が記載されています。興味のある方は、そちらも御覧になってみて下さい。

さて、初期入力モードを設定するレジストリ項目は、上のページで説明されている、HKEY_LOCAL_MACHINE\Software\Microsoft\IMEJP\3.1\MSIME キーの下の option3 です。この値は、デフォルトでは「ひらがな」入力モードになっています。これを変更して、たとえば「半角英数」入力モードにしたい場合は、0×0000000C を指定します。ただし、0×0000000C そのものを設定すると、「かな入力」になってしまいます。「ローマ字入力」にしたい場合は(※上のページに説明がある通り、デフォルトは「ローマ字入力です)、ローマ字入力を指定する 0×00000001 と組み合わせた値(論理 OR 結合した値)の 0×0000000D を指定して下さい。

つまり、OS Design のレジストリ設定ファイル(OSDesign.reg や project.reg)に次の行を追加して OS イメージをビルドすると、日本語 IME の初期入力モードが「半角英数」になります。

[HKEY_LOCAL_MACHINE\Software\Microsoft\IMEJP\3.1\MSIME]
    "option3"=dword:0000000D

■ソフトウェアキーボードの設定
次に、ソフトウェアキーボード(Software-based Input Panel; SIP)に関する tips です。デフォルトの振る舞いでは、エディットフィールドなど、テキスト入力の GUI 部品にフォーカスが当たると、自動的に SIP が表示されますが、WEC 7 では、この振る舞いを変更できます。

WEC 7 の SIP のレジストリ設定項目のリファレンスは、次のページです:

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

このページにある HKEY_CURRENT_USER\ControlPanel\SIP キーの説明を見て下さい。TurnOffAutoDeploy という値を 1 にすると、SIP が自動的に表示されない(are not auto deployed)と書かれています。実際、OSDesign.reg に次の行を追加して OS イメージをビルドすると、SIP が自動表示されません:

[HKEY_CURRENT_USER\ControlPanel\Sip]
    "TurnOffAutoDeploy"=dword:1


注意:リファレンスでは、キーの名前は ‘HKEY_CURRENT_USER\ControlPanel\SIP’ となっていますが、実際は、上の設定例のように、末尾は ‘Sip’ (先頭のみ大文字)が正しいようです。WINCE700\public\COMMON\oak\files/common.reg の記述を見ても、そうなっています。

なお、WinCE 6.0 のリファレンスを見ると、HKEY_CURRENT_USER\ControlPanel\SIP キーに TurnOffAutoDeploy という値がありません:

 Input Panel Registry Settings (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee504097(v=winembedded.60).aspx

WinCE 6.0 の OS Design に上記の TurnOffAutoDeploy の設定を追加して OS イメージをビルドしてみても、SIP が自動表示される動作は変わりません。つまり、WinCE 6.0 までは undocumented だったが実は使えた、という機能ではなく、WEC 7 になって初めて使えるようになった機能のようです。

しかし、Windows Mobile では、TurnOffAutoDeploy の設定が有効だったようで、’TurnOffAutoDeploy’ で Web を検索すると、Windows Mobile の tips として紹介しているページがヒットします。たとえば、次のページです。

 How To Stop Your SIP From Automatically Popping Up
 http://pocketnow.com/tweaks-hacks/how-to-stop-your-sip-from-automatically-popping-up

 Windows Mobile Registry Tricks
 http://www.ehow.com/list_7200029_windows-mobile-registry-tricks.html

Windows Mobile(6.x)のカーネルは WinCE 5.0 だったようですから、念のためにと思い、WinCE 5.0 のリファレンスも見てみましたが、WinCE 6.0 と同様、TurnOffAutoDeploy の記述は、ありません:

 Input Panel Registry Settings (Windows CE 5.0)
 http://msdn.microsoft.com/en-us/library/aa452674.aspx 

真相は分かりませんが、Windows Mobile でのみ提供されていた機能が、ユーザや開発者からの要望により WEC 7 にも追加された、ということなのかも知れません。

Add comment 2012/12/31 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

第3回Device2Cloud Contest 決勝大会

今週の土曜日(12/8)、東京電機大学の北千住キャンパス丹波ホール1号館で、第3回目の「Device2Cloud コンテスト」の決勝大会が開催されます。タイムテーブルが、次のページで公開されています:

 http://www.d2c-con.com/D2C3rd_TimeTable.pdf

入場無料、見学自由ですので、興味のある方は是非ご参加下さい。

余談ですが、当日、会場内に設けられる展示スペースでは、弊社が 11/14-16 の ET 2012 に出展した M-TEC 社のドライビングアシスト・コンソールも展示する予定です。このデバイスについては、先日、@IT MONOist の記事で取り上げて頂きました:

 「無限のGTレースカー、ステアリングのど真ん中にWindowsシステムを搭載」
 http://monoist.atmarkit.co.jp/mn/articles/1211/20/news010.html

Add comment 2012/12/06 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

telnetd と ftpd のユーザ認証

WEC 7/WinCE 6.0 には、皆さんお馴染みの Telnet サーバ(telnetd)と FTP サーバ(ftpd)が付属しています。どちらも、ユーザ認証なしで動かすことができ、開発中のデバイスを、ネットワーク経由で遠隔操作する際に便利です。しかし、ユーザ認証なしのままの設定では、セキュリティ面で大きなリスクがあります。今回は、WEC 7/WinCE 6.0 付属の telnetd と ftpd について、ユーザ認証を有効にする場合に必要な手順を述べます。

■はじめに
telnetd と ftpd は、OS Design のカタログビューでは、次の場所にあります:

・WEC 7 のカタログ項目
 <OS Design 名>
  Core OS
   Windows Embedded Compact
    Communication Services and Networking
     Servers
★     FTP Server
★     Telnet Server

・WinCE 6.0 のカタログ項目
 <OS Design 名>
  コア OS
   CEBASE
    通信サービスおよびネットワーク
     サーバー
★     FTP サーバー
★     Telnet サーバー

これらのカタログ項目を選択することにより、telnetd と ftpd が OS イメージに組み込まれます。

■ユーザ認証無しで動かす場合の設定
冒頭で述べたように、telnetd と ftpd のどちらも、レジストリ設定により、ユーザ認証無しで動かすことができます。次の行を、OS Design のレジストリ設定ファイル、つまり、OSDesign.reg(WEC 7 の場合)や project.reg(WinCE 6.0 の場合)に記述すれば、ユーザ認証無しで動作します。

[HKEY_LOCAL_MACHINE\Comm\TELNETD]
    "IsEnabled"=dword:1
    "UseAuthentication"=dword:0

[HKEY_LOCAL_MACHINE\Comm\FTPD]
    "IsEnabled"=dword:1
    "AllowAnonymous"=dword:1
    "AllowAnonymousUpload"=dword:1
    "NoSystemInfo"=dword:0


telnetd は、UseAuthentication に0を指定すると、接続時にユーザ認証を行いません。ftpd は、AllowAnonymous に1を指定すると、匿名アクセスを許可します。telnetd と ftpd に対するレジストリ設定項目の詳細については、リファレンスの次のページをご覧下さい。

 
 Telnet Server Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee499490

 FTP Server Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee498908

上のページは、WEC 7 のリファレンスですが、WinCE 6.0 の場合も同じです。なお、FTP サーバのレジストリ設定のページ(上の二番目のページ)にある DefaultDir の説明にあるように、デフォルトでは、FTP アカウントのルートディレクトリは /Temp に設定されます。WEC 7/WinCE のルートディレクトリを FTP アカウントでもルートディレクトリとしたい場合は、DefaultDir の値を “\\” にして下さい。

■ユーザ認証を有効にするための設定
telnetd と ftpd のユーザ認証無しのアクセスを禁止して、必ずユーザ認証を行わせるには、レジストリ設定を次のようにして下さい。

  • telnetd のレジストリ設定で、UseAuthentication を指定しないか(デフォルト値は0)、または、明示的に0を指定する。
  • ftpd のレジストリ設定で、AllowAnonymous と AllowAnonymousUpload を指定しないか(デフォルト値は0)、または、明示的に0を指定する。
  • アクセスを許可するユーザを、UserList で指定する。

たとえば、’Administrator’ というユーザにのみアクセスを許可する場合は、レジストリ設定ファイルに次の記述を入れて下さい。

[HKEY_LOCAL_MACHINE\Comm\TELNETD]
    "IsEnabled"=dword:1
    "UseAuthentication"=dword:1
    "UserList"="Administrator"

[HKEY_LOCAL_MACHINE\Comm\FTPD]
    "IsEnabled"=dword:1
    "NoSystemInfo"=dword:1
    "UserList"="Administrator"


上の記述例では、ftpd のレジストリ設定で NoSystemInfo に1を設定することにより、FTP クライアントがシステム情報を要求しても返さないようにしています。telnetd と ftpd のどちらも、平文でパスワードを受け取りますので、セキュリティが高くありません(※そのため、OS Design のこれらのカタログ項目を選択すると、Platform Builder がセキュリティリスクの警告ダイアログを表示します)。多少ともセキュリティを高める方策として、NoSystemInfo に1を指定するのは、悪くない考えだと思います。

さて、これだけでは、telnetd と ftpd にユーザ Administrator で接続することは、できません。次の二つの手順が、追加で必要です:

  1. OS Design に、ユーザ認証のカタログ項目を追加する。
  2. ユーザ登録を実行する。

以下、これらについて順に述べます。

(1)ユーザ認証のカタログ項目
以前に書いたエントリ(「WEC/WinCE から共有ディレクトリ(ファイルサーバ)をアクセス」)で、SMB/CIFS クライアントを組み込む場合には、認証処理に必要なコンポーネントも組み込む必要があると書きました。これと同様に、telnetd と ftpd のユーザ認証機能を有効にする場合も、認証処理用のコンポーネントを組み込まなければいけません。

telnetd と ftpd のユーザ認証では、認証処理用のコンポーネントとして必要なのは NTML です。WEC 7 の場合であれば、次のカタログ項目を選択して下さい。

 <OS Design 名>
  Core OS
   Windows Embedded Compact
    Security
     Authentication Services (SSPI)
★     NTLM

(2)ユーザ登録
ユーザ登録を行うには、アプリケーションから NTLMSetUserInfo() を呼び出して下さい。この関数で、ユーザのアカウント名とパスワードを設定できます。たとえば、OS の起動完了後に自動実行されるアプリケーションを実装し、そのアプリケーションが、NTLMSetUserInfo() を呼び出して telnetd と ftpd のユーザアカウントを登録する、というのが、お手軽な方策でしょう。NTLMSetUserInfo() については、リファレンスの次のページをご覧下さい:

 NTLMSetUserInfo (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee498121

ところで、telnetd と ftpd のレジストリ項目の UserList では、アクセスを許可するユーザを複数指定できるのですが、リファレンスの説明に間違いがありますので、注意して下さい。リファレンスには、許可対象のユーザが複数ある場合、カンマ区切りで指定すると書かれていますが、これはセミコロン区切りの間違いです。WinCE 6.0 のリファレンスの Telnet サーバに関するページには、この間違いを指摘するコメントが寄せられています:

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

上のページの末尾にある Community Content の欄に、”Telnet Server Registry Settings corrections” というコメントがあり、上記の間違いを指摘しています。

UserList の記法の、より正確な説明は、telnetd と ftpd がユーザ認証時に利用している AuthHelpValidateUser() という API 関数を宣言したヘッダファイルに記載されています。この API 関数は、リファレンスに載っていないのですが、
 %_WINCEROOT%/public/common/sdk/inc/authhlp.h
で宣言されており、この authhlp.h の冒頭のコメント中に、より正確な記法の説明があります。その説明によれば、許可するユーザに加えて、拒否するユーザの指定も可能です。

■telnetd と ftpd のソースコード
今回のエントリの最後に、telnetd と ftpd のソースコードの場所を紹介しておきます。リファレンスページにも記載されているのですが、これらのソースコードは、次の場所にあります:

 telnetd
 %_WINCEROOT%/public/servers/sdk/samples/telnetd/

 ftpd
 %_WINCEROOT%/public/servers/sdk/samples/ftpd/

samples というディレクトリの配下に収録されていることからも分かるように、これらは、どちらかといえばサンプルコードという扱いです。実装上の制限が加えられている個所もありますので(たとえば、telnted は、同時接続数は最大9です)、必要であれば、カスタマイズを加えて利用するのが良いでしょう。telnetd のソースコードを改変し、限られたコマンドしか受け付けないようにすることでセキュリティを高める、というのは、その一例だと思います。

Add comment 2012/08/18 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

Next Posts Previous Posts


Categories

Links

Posts by Authors

Recent Posts

Calendar

2017年10月
« 9月    
1234567
891011121314
15161718192021
22232425262728
293031  

Posts by Month

Posts by Category

Meta