Archive for 2012/12

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


Categories

Links

Posts by Authors

Recent Posts

Calendar

2012年12月
« 8月   1月 »
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Posts by Month

Posts by Category

Meta