KMDF のディスパッチルーチンについて

皆さん、はじめまして。WDK サポートチームの JS と申します。I 沢さんに続いて新しくチームに加入いたしましたので、これからよろしくお願いします。

今回は、WDM と KMDF のディスパッチルーチンの登録方法の違いについてご紹介したいと思います。

 

WDFの概要

さて、いきなり出てきた KMDF という単語ですが、これは Kernel Mode Driver Framework の略語で、WDF (Windows Driver Foundation) というドライバモデルに分類されます。

WDF や KMDF についてもうご存知でいらっしゃる方が多いと思いますが、このブログではあまり触れられていません。なので、本題に入る前に、まずはその概要をご紹介したいと思います。

 

なおきお~さんの記事「日本語の資料はあるの?」でも書かれていますが、WDF には二種類のドライバーがあります:

・KMDF (Kernel Mode Driver Framework)

・UMDF (User Mode Driver Framework)

WDFの前に開発されたWDM (Windows Driver Model) というモデルがありますが、後者の方はカーネルモードのドライバしか作成できません。また、ユーザーモードドライバ開発用フレームワークは、WDFの誕生と同時に提供されました。一方、カーネルモードドライバは KMDF と WDM の二種類のモデルができました。

 

上記のように、KMDF は WDM を基にした新しいドライバーモデルであり、デザインに大きな違いがあります。どういう違いがあるのか、挙げたらきりがありませんので、まずは代表的な変更点である 「ディスパッチルーチン (Dispatch Routine) の登録方法の違い」 をソースコードを例としてお見せしたいと思います。

ちなみに、ディスパッチルーチンと言うのは、アプリケーションやOSから来るリクエスト等を処理するための仕組みです。

 

WDMのディスパッチルーチンの登録方法

では早速、WDM と KMDF でどのようにディスパッチルーチンが登録されているか見てみましょう。

WDM のおおまかな仕様については、K里さんの記事 「DriverObject と DriverEntry」 で書かれていますので、そちらの方もご覧になっていただければと思います。

 

まず、WDM のディスパッチルーチンの登録方法からおさらいしてみましょう。

 

NTSTATUS

DriverEntry(

    __in PDRIVER_OBJECT DriverObject,

    __in PUNICODE_STRING RegistryPath

    )

{

    DriverObject->MajorFunction [IRP_MJ_CREATE] =

    DriverObject->MajorFunction [IRP_MJ_CLOSE] = Bus_CreateClose;

    DriverObject->MajorFunction [IRP_MJ_PNP] = Bus_PnP;

    DriverObject->MajorFunction [IRP_MJ_POWER] = Bus_Power;

    DriverObject->MajorFunction [IRP_MJ_DEVICE_CONTROL] = Bus_IoCtl;

    DriverObject->MajorFunction [IRP_MJ_SYSTEM_CONTROL] = Bus_SystemControl;

    DriverObject->DriverUnload = Bus_DriverUnload;

    DriverObject->DriverExtension->AddDevice = Bus_AddDevice;

 

 

WDM では DRIVER_OBJECT
構造体のメンバを直接アクセスしてディスパッチルーチン関数を登録していきます。

 

KMDF のディスパッチルーチンの登録方法

次は KMDF のディスパッチルーチンの登録方法を見てみます。

 

NTSTATUS

DriverEntry(

    IN PDRIVER_OBJECT DriverObject,

    IN PUNICODE_STRING RegistryPath

    )

{

    WDF_DRIVER_CONFIG config;

    WDF_DRIVER_CONFIG_INIT(

        &config,

        EvtDriverDeviceAdd

        );

 

    status = WdfDriverCreate(DriverObject,

                             RegistryPath,

                             WDF_NO_OBJECT_ATTRIBUTES,

                             &config,

                             &driver);

 

 

大まかな流れとしては以下の通りになります。

  1. WDF_DRIVER_CONFIG 構造体の変数 (config) を WDF_DRIVER_CONFIG_INIT 関数で初期化する
  2. 初期化した config を基に WdfDriverCreate 関数でドライバオブジェクトを作成する

 

ご覧のとおり、WDM の場合とは全く別の形をしたコードとなっています。KMDF ではここでディスパッチルーチンの登録を行っていない点にご留意ください。

唯一、WDF_DRIVER_CONFIG_INIT 関数で、EvtDriverDeviceAdd という関数だけを登録しています。

それぞれの関数の詳細については以下のドキュメントをご覧ください。

 

WDF_DRIVER_CONFIG_INIT 関数

https://msdn.microsoft.com/ja-jp/library/ff551302(v=vs.85).aspx

WdfDriverCreate メソッド

https://msdn.microsoft.com/ja-jp/library/ff547175(v=vs.85).aspx

 

 

デバッガーによる確認

次に、このように実装された WDM と KMDF それぞれのドライバで、ディスパッチルーチンがどのように登録されているのかデバッガーを使って見てみましょう。

まずは WDM の方から:

 

kd> !drvobj busenum 7

Dispatch routines:

 

[00] IRP_MJ_CREATE b1021610 busenum!Bus_CreateClose

[01] IRP_MJ_CREATE_NAMED_PIPE 804fb739 nt!IopInvalidDeviceRequest

[02] IRP_MJ_CLOSE b1021610 busenum!Bus_CreateClose

[03] IRP_MJ_READ 804fb739 nt!IopInvalidDeviceRequest

[04] IRP_MJ_WRITE 804fb739 nt!IopInvalidDeviceRequest

[05] IRP_MJ_QUERY_INFORMATION 804fb739 nt!IopInvalidDeviceRequest

 

 

WDM では、DriverObject にディスパッチルーチンがいくつか設定されているのが確認できます。

 

一方、KMDF の方を見ると:

 

kd> !drvobj dynambus 7

Dispatch routines:

[00] IRP_MJ_CREATE 86217d1c Wdf01000!FxDevice::DispatchWithLock

[01] IRP_MJ_CREATE_NAMED_PIPE 86217d1c Wdf01000!FxDevice::DispatchWithLock

[02] IRP_MJ_CLOSE 86217d1c Wdf01000!FxDevice::DispatchWithLock

[03] IRP_MJ_READ 86217d1c Wdf01000!FxDevice::DispatchWithLock

[04] IRP_MJ_WRITE 86217d1c Wdf01000!FxDevice::DispatchWithLock

[05] IRP_MJ_QUERY_INFORMATION 86217d1c Wdf01000!FxDevice::DispatchWithLock

 

 

このように、すべてのルーチンが抽象化されて登録されていることがわかります。

ただ、前述したように KMDF 版ではディスパッチルーチンを登録していません。上記コード例の通り KMDF の DriverEntry で登録したのは EvtDriverDeviceAdd 関数だけなのです。

では、そのほかのディスパッチルーチンはどうやって登録されたのでしょう。

 

実は、デフォルトの関数がフレームワークによって自動的に登録されているのです。

そして、すべての関数が同じ Wdf01000!FxDevice::DispatchWithLock という関数になっています。WDM では直接、該当する関数が呼ばれていたものが、KMDF ではフレームワークが用意したラッパー関数を通して間接的に処理されるようになっているのです。

 

ですが、詳細なデバイス制御をしたいときには独自の関数を登録します。その場合は、 DriverEntry で唯一登録した EvtDriverDeviceAdd 関数内で登録をおこないます。

ちなみに、KMDF で登録する関数は「コールバック関数」と呼ばれています。

 

では、例を見てみましょう:

 

NTSTATUS

EvtDriverDeviceAdd(

    IN WDFDRIVER Driver,

    IN PWDFDEVICE_INIT DeviceInit

    )

{

    WDF_CHILD_LIST_CONFIG config;

    WDF_OBJECT_ATTRIBUTES fdoAttributes;

    NTSTATUS status;

    WDFDEVICE device;

 

    WDF_IO_QUEUE_CONFIG queueConfig;

    WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;

    WDFQUEUE queue;

 

    UNREFERENCED_PARAMETER(Driver);

 

    WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);

 

    pnpPowerCallbacks.EvtDevicePrepareHardware = DriverEvtPrepareHardware;

    pnpPowerCallbacks.EvtDeviceReleaseHardware = DriverEvtReleaseHardware;

    pnpPowerCallbacks.EvtDeviceSelfManagedIoInit =

DriverEvtDeviceSelfManagedIoInit;

 

    pnpPowerCallbacks.EvtDeviceD0Entry = DriverEvtDeviceD0Entry;

    pnpPowerCallbacks.EvtDeviceD0Exit = DriverEvtDeviceD0Exit;

 

    WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);

 

    status = WdfDeviceCreate(&DeviceInit, &fdoAttributes, &device);

 

    WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig,

                             WdfIoQueueDispatchParallel);

 

    queueConfig.EvtIoDeviceControl = EvtDriverIoDeviceControl;

 

    status = WdfIoQueueCreate(device,

                              &queueConfig,

                              WDF_NO_OBJECT_ATTRIBUTES,

                              &queue

                              );

 

このコードでは、PNP 及びパワーマネジメントに関するコールバック関数

(WDF_PNPPOWER_EVENT_CALLBACKS 構造体に登録される関数群) と、I/O キューに関するコールバック関数 (WDF_IO_QUEUE_CONFIG 構造体に登録される関数群) の登録を行っています。

 

登録までのステップは大きく分けると 3 つになります:

 

  1. コールバック関数を管理する構造体(WDF_PNPPOWER_EVENT_CALLBACKS 構造体の変数 pnpPowerCallbacks, WDF_IO_QUEUE_CONFIG構造体の変数 queueConfig)を初期化する
  2. 必要な関数を上記の構造体に割り当てる
  3. コールバック関数を登録する関数(WdfDeviceInitSetPnpPowerEventCallbacks,
    WdfIoQueueCreate)を呼ぶ

 

また、KMDF の方では、WDFDEVICE_INIT 構造体 (変数はDeviceInit) や WDFDEVICE 構造体 (変数はdevice) といった、デバイスに関する構造体にコールバック関数を登録されており、ドライバを管理するWDFDRIVER 構造体(変数はDriver)には触れていません。

 

上記コードにある関数にそれぞれの仕様がございますので、ご確認ください。

例として、以下のドキュメントをご覧になっていただければと思います。

 

I/O キューの作成

https://msdn.microsoft.com/ja-jp/library/ff540783(v=vs.85).aspx

WdfDeviceInitSetPnpPowerEventCallbacks メソッド

https://msdn.microsoft.com/ja-jp/library/ff546135(v=vs.85).aspx

EvtIoDeviceControl

https://msdn.microsoft.com/ja-jp/library/ff541758(v=vs.85).aspx

 

――

 

比較はこれくらいにして、結論に入りたいと思います。

 

先ほど「KMDFはWDMを基にした」と書きました通り、動作的にはモデル間に違いはありません。では、なぜわざわざ新しいモデルを作ったのでしょうか?

WDMではモデルの全体的な仕組みをローレベルで理解できるスキルが必要とされていましたが、それでは敷居が高すぎるのではないかということで、オブジェクト指向の特徴を活かしたWDFが生まれたのです。つまり、フレームワークがドライバを実装する際に共通で必要な処理を行ってくれるので、開発者はドライバの対象となるデバイスに合わせた関数をデザインするだけで大丈夫、という意図で作られたモデルということになります。

 

今回は「ディスパッチルーチンの登録方法」という観点で、WDMとKMDFの違いを紹介しただけであって、WDMとの良し悪しを比べているわけではありません。KMDFの方が楽という方もいれば、WDMの方が自由度があるという方もいますし、結局は自分の性格や目的にあったモデルを選ぶのが一番だと思います。

 

ご興味のある方は、 WDM と KMDF をどちらもお試しいただいて、その違いを実感していただければ幸いです。