Windowsデバイスドライバの基本動作を確認する (2) : プラグアンドプレイ

2017年7月11日

はじめに

前回の記事では、Windowsカーネルモードドライバの基本的なロード・アンロードの動きを確認しました。このときは、OSに標準で搭載されているSCというコマンドを使って、手動でサービスの開始・停止を行うことで、事前に登録されたドライバがロードまたはアンロードされました。しかし、PCに接続されたデバイスを実際に制御するドライバは、OSがそのデバイスの存在を検出したのを受けて、それに対応するドライバが自動的にロードされ、動作を開始します。同様に、デバイスが切断されれば、ドライバは動作を停止してアンロードされます。今回は、やはり簡単なドライバを作成して実際に動かしながら、基本的なプラグアンドプレイ(PnP)の流れを確認していきます。

Windows Driver Model

Windows Driver Model (WDM)は、Windows 98とWindows 2000で導入されたデバイスドライバのフレームワークで、Windows NT 4.0など、それ以前のOSのドライバに比べて、PnPや電源管理(パワーマネジメント)などの共通の枠組みが追加された形になっています。詳細は、例えば以下のサイトで説明されています。
https://msdn.microsoft.com/en-us/library/windows/hardware/ff565698(v=vs.85).aspx

前回作成したドライバはWDMドライバではなく、PnPはサポートしていませんが、WDMドライバの初期化や終了の処理自体は、従来のWindows NTのドライバから受け継がれています。したがって、従来型のドライバは、Non-PnPドライバや、レガシードライバと呼ばれています。

WDMドライバは、その役割によって大きく以下の3種類に分類されます。

  • ファンクションドライバ
    • 対象のデバイスを主に制御するドライバ
  • バスドライバ
    • デバイスが接続されるバスを管理するドライバ
    • バスに接続されたデバイスを検出して、対応するドライバがロードされるために必要な処理を行う
  • フィルタドライバ
    • 必要に応じてファンクションドライバの上位または下位に挿入されるオプションのドライバ
    • デバイスに対して補助的な制御や付加機能の提供を行う

先ほど、OSがデバイスの存在を検出と述べましたが、実際にはバスドライバが検出します。また、バスドライバは、あるバス (例えば、PCI ExpressやUSB) を管理するものですが、バスに接続されたデバイスの検出はそのバスのコントローラ (例えば、PCI ExpressのRoot ComplexやUSBのHost Controller) を制御することで行っており、バスドライバはそのコントローラを制御するファンクションドライバとしての機能もあわせもっています。

各ドライバは階層構造をなし、デバイスオブジェクトと呼ばれるデータ構造を用意して、ドライバ間でIRP (I/Oリクエストパケット)と呼ばれるデータをやり取りすることで、データの入出力や機器の制御を行います。あるデバイスを制御するドライバ階層において、各ドライバが生成して管理するデバイスオブジェクトは、それぞれ以下のように呼ばれています。

  • ファンクションドライバ : 機能デバイスオブジェクト (FDO : Functional Device Object)
  • バスドライバ : 物理デバイスオブジェクト (PDO : Physical Device Object)
  • フィルタドライバ : フィルタデバイスオブジェクト

ドライバの階層構造およびデバイスオブジェクトの接続構成の例を以下に示します。

今回は、WDKに付属のサンプルバスドライバを利用して仮想的なデバイスを生成し、それを制御するための簡単なファンクションドライバを作成しました。

簡単なWDMドライバの作成

前回作成したNon-PnPのドライバでは、ドライバアンロード時に実行されるコールバック関数(DriverUnload)のみを実装して、ドライバオブジェクトに登録しました。WDMドライバ (ファンクションドライバ) の場合、少なくとも以下のコールバック関数を登録する必要があります。

  • AddDevice
    • デバイスオブジェクトを生成して、ドライバスタックに接続する
  • MajorFunction [IRP_MN_PNP]
    • デバイスの開始、停止、削除など、PnPに関する各種処理を行う
  • MajorFunction [IRP_MN_POWER]
    • サスペンドやレジュームなど電源管理に関する各種処理を行う

MajorFunctionは、各メジャーファンクション番号に対するコールバック関数のポインタの配列になっており、メジャーファンクション番号ごとに別々の関数を登録することも、あるいは、全てのメジャーファンクション番号に対して同じ関数を登録して、その中でIRPで指定されたメジャーファンクション番号を見て処理を分けることも可能です。

デバイス追加処理

デバイス追加処理では、まず、IoCreateDevice関数を呼び出してデバイスオブジェクトを生成します。
https://msdn.microsoft.com/en-us/library/windows/hardware/ff548397(v=vs.85).aspx

    Status = IoCreateDevice(
                DriverObject,
                sizeof(DEVICE_EXTENSION),   // DeviceExtensionSize
                NULL,                       // DeviceName
                FILE_DEVICE_UNKNOWN,        // DeviceType
                0,                          // DeviceCharacteristics
                FALSE,                      // Exclusive
                &DeviceObject
                );
    if (!NT_SUCCESS(Status)) {
        DbgPrint("IoCreateDevice failed\n");
        return Status;
    }

この際、引数のDeviceExtensionSizeとして以下の構造体のサイズを指定しています。この構造体は、直下のドライバのデバイスオブジェクトのポインタを保存し、この後の処理で下位ドライバにIRPを送信する際に使用します。

typedef struct _DEVICE_EXTENSION {
    PDEVICE_OBJECT NextDeviceObject;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

続いて、IoAttachDeviceToDeviceStack関数を呼び出して、生成したデバイスオブジェクトを下位ドライバに接続します。IoAttachDeviceToDeviceStack関数の戻り値は直下のドライバが生成したデバイスオブジェクトのポインタになるので、それをDEVICE_EXTENSION構造体に格納しています。
https://msdn.microsoft.com/en-us/library/windows/hardware/ff548300(v=vs.85).aspx

    DeviceExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
    DeviceExtension->NextDeviceObject = IoAttachDeviceToDeviceStack(
                                            DeviceObject,           // SourceDevice
                                            PhysicalDeviceObject    // TargetDevice
                                            );
    if (DeviceExtension->NextDeviceObject == NULL) {
        DbgPrint("IoAttachDeviceToDeviceStack failed\n");
        IoDeleteDevice(DeviceObject);
        return STATUS_NO_SUCH_DEVICE;
    }

PnP処理

PnP処理では、IRPのマイナーファンクション番号で指定された要求内容に従って、デバイスの開始や停止などの処理を行います。

デバイスの削除要求 (IRP_MN_REMOVE_DEVICE) では、IoDetachDevice関数を呼び出してデバイスオブジェクトを下位のデバイススタックから切断した後、IoDeleteDevice関数を呼び出してデバイスオブジェクトを削除します。
https://msdn.microsoft.com/en-us/library/windows/hardware/ff549087(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/windows/hardware/ff549083(v=vs.85).aspx

一方、他の要求については、このドライバ内では具体的な処理は何も行わず、IRPを一旦下位ドライバの送信した後、その結果が返ってきてから、単純にIRPを完了させるだけになっています。

    PDEVICE_EXTENSION DeviceExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
    PIO_STACK_LOCATION IrpSp = IoGetCurrentIrpStackLocation(Irp);
    NTSTATUS Status;

    DbgPrint("PNP: MinorFunction=0x%x\n", IrpSp->MinorFunction);
    if (IrpSp->MinorFunction == IRP_MN_REMOVE_DEVICE) {
        // デバイス削除時の処理
        IoSkipCurrentIrpStackLocation(Irp);
        Status = IoCallDriver(DeviceExtension->NextDeviceObject, Irp);
        IoDetachDevice(DeviceExtension->NextDeviceObject);
        IoDeleteDevice(DeviceObject);
    } else {
        // それ以外の処理
        Status = SampleSendIrpSynchronously(DeviceExtension, Irp);
        Irp->IoStatus.Status = Status;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }
    return Status;

IRPを下位ドライバに送ってから、その結果を受け取るための手順は、以下のコードにより実現しています。

  1. イベントオブジェクトを初期化
  2. IRPのスタック情報を次の階層用のスタックにコピー
  3. IRPの完了ルーチン (SampleCompletionRoutine) を設定
  4. IRPを下位ドライバに送信
  5. IRPが完了してイベントオブジェクトがセットされたら、結果を取り出す
    // イベントオブジェクトを初期化
    KeInitializeEvent(&Event, NotificationEvent, FALSE);

    // IRPのスタックを下位ドライバのスタックにコピー
    IoCopyCurrentIrpStackLocationToNext(Irp);

    // IRPの完了ルーチンを指定
    IoSetCompletionRoutine(Irp, SampleCompletionRoutine, &Event, TRUE, TRUE, TRUE);

    // IRPを下位ドライバに送信
    Status = IoCallDriver(DeviceExtension->NextDeviceObject, Irp);
    if (Status == STATUS_PENDING) {
        KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, NULL);
        Status = Irp->IoStatus.Status;
    }I

IRPの完了ルーチン (SampleCompletionRoutine) では、以下のようにイベントオブジェクトをセットして、上記処理にIRPの実行結果を通知しています。

    if (Irp->PendingReturned) {
        KeSetEvent((PKEVENT)Context, IO_NO_INCREMENT, FALSE);
    }
    return STATUS_MORE_PROCESSING_REQUIRED;

電源管理処理

今回作成したドライバは、電源管理については何も行っておらず、受信したIRPを単純に下位ドライバに転送するだけになっています。このケースは、先ほどと異なり、IRPの実行結果をもらうための完了ルーチンの指定は行わないため、最下位のドライバがIRPを完了させた後は、このドライバを経由せずに、IRP発行元に結果が返されます。

    Irp->IoStatus.Status = STATUS_SUCCESS;
    IoSkipCurrentIrpStackLocation(Irp);
    return IoCallDriver(((PDEVICE_EXTENSION)DeviceObject)->NextDeviceObject, Irp);

インストールの仕組み

前回作成ドライバは、SCコマンドを用いてサービスを生成し、手動でサービスを開始することでドライバをロードしていました。WDMドライバは、バスドライバが検出したデバイスに対してOSが対応するドライバを自動的に起動するため、必要な情報をOSに設定するためにINFファイルを使用します。INFファイルの文法や作成方法については以下のサイトなどで説明されています。
https://docs.microsoft.com/en-us/windows-hardware/drivers/install/overview-of-inf-files

作成したINFファイルを以下に示します。

[Version]
Signature="$WINDOWS NT$"
Class=System
ClassGuid={4D36E97D-E325-11CE-BFC1-08002BE10318}
Provider="Hiromitsu Sakamoto"
DriverVer=07/02/2017,6.1.7600.16385
CatalogFile=sample2.cat

[DestinationDirs]
DefaultDestDir=12

[SourceDisksNames]
1="Setup Disk"

[SourceDisksFiles]
sample2.sys=1

[Manufacturer]
"(Standard system devices)"=Standard

[Standard]
"Microsoft Toaster"=SAMPLE2,{b85b7c50-6a01-11d2-b841-00c04fad5171}\MsToaster

[SAMPLE2.NT]
CopyFiles=SAMPLE2.CopyFiles

[SAMPLE2.CopyFiles]
sample2.sys

[SAMPLE2.NT.Services]
AddService=sample2,2,SAMPLE2.AddService

[SAMPLE2.AddService]
DisplayName="Sample WDM Driver"
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%12%\sample2.sys

INFファイルでは、ドライバファイルの名前やコピー先、サービス名やサービスの属性 (前回作成したドライバでSCコマンドを使って設定した情報)のほか、このドライバのインストール対象であるデバイスの識別情報などを指定します。上記INFファイルでは、WDKのサンプルバスドライバが生成する仮想デバイスの識別子である “{b85b7c50-6a01-11d2-b841-00c04fad5171}\MsToaster” を指定しています。これ以外に、ドライバ固有のレジストリパラメータを設定することもできますが、説明は割愛します。

動作確認

今回作成したドライバの動作確認には、WDKのサンプルtoasterのバスドライバを使用しました。Toasterのバスドライバは、やはりサンプルとしてWDKに含まれているアプリケーションを用いて、仮想デバイスの追加・削除を行うことができます。

仮想デバイスの追加

Toasterの付属アプリケーションでは、以下の手順で新たな仮想デバイスを追加することができます。

Enum -p <シリアル番号>

仮想デバイスを追加すると、作成したWDMドライバがロードされ、デバイスの追加処理が行われます。

デバイスの追加処理でやり取りされるPnP関連のIRPのマイナーファンクション番号は以下の通りです。

  • IRP_MN_QUERY_LEGACY_BUS_INFORMATION (0x18)
  • IRP_MN_QUERY_RESOURCE_REQUIREMENTS (0xB)
  • IRP_MN_FILTER_RESOURCE_REQUIREMENTS (0xD)
  • IRP_MN_START_DEVICE (0x0)
  • IRP_MN_QUERY_CAPABILITIES (0x9)
  • IRP_MN_QUERY_PNP_DEVICE_STATE (0x14)
  • IRP_MN_QUERY_DEVICE_RELATIONS (0x7)

各マイナーファンクションの詳細は以下のサイトで説明されています。
https://msdn.microsoft.com/en-us/library/windows/hardware/ff558807(v=vs.85).aspx

仮想デバイスの削除

Toasterの付属アプリケーションでは、以下の手順で既存の仮想デバイスを削除することができます。

Enum -u <シリアル番号>

仮想デバイスを削除すると、デバイスの削除処理が行われたのち、ドライバがアンロードされます。

デバイスの削除処理でやり取りされるPnP関連のIRPのマイナーファンクション番号は以下の通りです。

  • IRP_MN_QUERY_DEVICE_RELATIONS (0x7)
  • IRP_MN_SURPRISE_REMOVAL (0x17)
  • IRP_MN_REMOVE_DEVICE (0x2)

このケースは、仮想デバイスを強制的に削除しており、アプリケーションとデバイスの間でデータ入出力も強制的に中断して削除処理を行うことから、Surprise Removalと呼ばれています。これに対して、デバイスマネージャ上でデバイスを無効にしたり、リムーバブルディスクの安全な取り外し操作では、OSからドライバに対して削除可能かどうかの伺いを立てて、削除できる場合のみ削除処理を行います。

おわりに

今回は、簡単なWDMドライバを作成して、PnPの基本的な動きを確認しました。最近は、より簡単にドライバを作成するための枠組みであるWindows Driver Foundation (WDF)を使用するのが主流かもしれませんが、今回は基本的な処理の流れを追う意味で、WDMを使用しました。別途機会があれば、WDFについても確認を行おうと思います。

About Author

hiro.sakamoto

Leave a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Recent Comments

Social Media