このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
前回の記事では、Windowsカーネルモードドライバの基本的なロード・アンロードの動きを確認しました。このときは、OSに標準で搭載されているSCというコマンドを使って、手動でサービスの開始・停止を行うことで、事前に登録されたドライバがロードまたはアンロードされました。しかし、PCに接続されたデバイスを実際に制御するドライバは、OSがそのデバイスの存在を検出したのを受けて、それに対応するドライバが自動的にロードされ、動作を開始します。同様に、デバイスが切断されれば、ドライバは動作を停止してアンロードされます。今回は、やはり簡単なドライバを作成して実際に動かしながら、基本的なプラグアンドプレイ(PnP)の流れを確認していきます。
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リクエストパケット)と呼ばれるデータをやり取りすることで、データの入出力や機器の制御を行います。あるデバイスを制御するドライバ階層において、各ドライバが生成して管理するデバイスオブジェクトは、それぞれ以下のように呼ばれています。
ドライバの階層構造およびデバイスオブジェクトの接続構成の例を以下に示します。
今回は、WDKに付属のサンプルバスドライバを利用して仮想的なデバイスを生成し、それを制御するための簡単なファンクションドライバを作成しました。
前回作成したNon-PnPのドライバでは、ドライバアンロード時に実行されるコールバック関数(DriverUnload)のみを実装して、ドライバオブジェクトに登録しました。WDMドライバ (ファンクションドライバ) の場合、少なくとも以下のコールバック関数を登録する必要があります。
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処理では、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を下位ドライバに送ってから、その結果を受け取るための手順は、以下のコードにより実現しています。
// イベントオブジェクトを初期化
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のマイナーファンクション番号は以下の通りです。
各マイナーファンクションの詳細は以下のサイトで説明されています。
https://msdn.microsoft.com/en-us/library/windows/hardware/ff558807(v=vs.85).aspx
仮想デバイスの削除
Toasterの付属アプリケーションでは、以下の手順で既存の仮想デバイスを削除することができます。
Enum -u <シリアル番号>
仮想デバイスを削除すると、デバイスの削除処理が行われたのち、ドライバがアンロードされます。
デバイスの削除処理でやり取りされるPnP関連のIRPのマイナーファンクション番号は以下の通りです。
このケースは、仮想デバイスを強制的に削除しており、アプリケーションとデバイスの間でデータ入出力も強制的に中断して削除処理を行うことから、Surprise Removalと呼ばれています。これに対して、デバイスマネージャ上でデバイスを無効にしたり、リムーバブルディスクの安全な取り外し操作では、OSからドライバに対して削除可能かどうかの伺いを立てて、削除できる場合のみ削除処理を行います。
今回は、簡単なWDMドライバを作成して、PnPの基本的な動きを確認しました。最近は、より簡単にドライバを作成するための枠組みであるWindows Driver Foundation (WDF)を使用するのが主流かもしれませんが、今回は基本的な処理の流れを追う意味で、WDMを使用しました。別途機会があれば、WDFについても確認を行おうと思います。
コンピュータビジョンセミナーvol.2 開催のお知らせ - ニュース一覧 - 株式会社フィックスターズ in Realizing Self-Driving Cars with General-Purpose Processors 日本語版
[…] バージョンアップに伴い、オンラインセミナーを開催します。 本セミナーでは、...
【Docker】NVIDIA SDK Managerでエラー無く環境構築する【Jetson】 | マサキノート in NVIDIA SDK Manager on Dockerで快適なJetsonライフ
[…] 参考:https://proc-cpuinfo.fixstars.com/2019/06/nvidia-sdk-manager-on-docker/ […]...
Windowsカーネルドライバを自作してWinDbgで解析してみる① - かえるのほんだな in Windowsデバイスドライバの基本動作を確認する (1)
[…] 参考:Windowsデバイスドライバの基本動作を確認する (1) - Fixstars Tech Blog /proc/cpuinfo ...
2021年版G検定チートシート | エビワークス in ニューラルネットの共通フォーマット対決! NNEF vs ONNX
[…] ONNX(オニキス):Open Neural Network Exchange formatフレームワーク間のモデル変換ツー...
YOSHIFUJI Naoki in CUDAデバイスメモリもスマートポインタで管理したい
ありがとうございます。別に型にこだわる必要がないので、ユニバーサル参照を受けるよ...