1.はじめに
デバイスドライバの機能の必要理解を深めるための学習として、Windowsでデバイスドライバ開発を行う際に推奨されているフレームワーク「Windows Driver Foundation」(WDF)に含まれるUser Mode Driver,USB(UMDF V2)について、Visual Studioでスケルトンを作成した時に自動生成される関数の役割や満たしているドライバ機能について調べました。
2. USB ユーザーモードドライバスケルトンの作成方法
今回調査したスケルトンプログラムは以下の手順で作成します。
- VisualStudioを起動し、「新しいプロジェクトの作成」をクリック。
2.「テンプレートの検索」で「USB UMDF」を検索し、「User Mode Driver,USB(UMDF V2)」のプロジェクトテンプレートを選択。
3.任意のプロジェクト名、保存場所、ソリューション名を入力し、「作成」をクリック。
3. スケルトン生成時のグローバル関数と変数
スケルトンプログラムを作成すると、以下のグローバル関数と変数が生成されます。
グローバル関数・変数が同じ名前で定義されている理由
Windowsドライバでは、グローバル関数と同一名のグローバル変数の定義は、
WDFイベントへの登録対象となるコールバック用関数の宣言に相当しているため。
コールバック対象となる関数の宣言として、関数型の変数にて定義(宣言)することが慣例になっている。
User Mode Driver,USBスケルトン作成時に生成される、対象の変数定義一覧
DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD xxxxEvtDeviceAdd; ※
EVT_WDF_DEVICE_PREPARE_HARDWARE xxxxEvtDevicePrepareHardware;
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL xxxxEvtIoDeviceControl;
EVT_WDF_IO_QUEUE_IO_STOP xxxxEvtIoStop;
EVT_WDF_OBJECT_CONTEXT_CLEANUP xxxxEvtDriverContextCleanup;
※ xxxxにはプロジェクト名が入る。
4. デバイスドライバに必要な機能
デバイスドライバに必要となる機能の観点で関数を分類すると以下となる。
機能 | 関数名 | 備考 |
---|---|---|
ドライバのロード(初期化) | DriverEntry | ドライバを開発するために必ず必要な関数 |
ドライバのアンロード(解放) | EvtDriverContextCleanup | ドライバアンロード時にログ出力などの処理を行わないなら不要 |
デバイスの初期化 | EvtDeviceAdd CreateDevice QueueInitialize |
PnPドライバを開発する場合必要。 物理デバイスが接続されたときに認識して通信するための初期化を行う |
デバイスのアンロード(解放) | EvtIoStop | アプリケーションに対してインターフェースを公開する場合に必要 |
デバイスの制御 (デバイスへの書き込み、デバイスからの読み込み、デバイスからの割込) |
EvtDevicePrepareHardware | PnPドライバを開発する場合必要。 物理デバイスが接続されたときに認識して通信するための初期化を行う |
アプリケーション要求の処理 | EvtIoDeviceControl | アプリケーションに対してインターフェースを公開する場合に必要 |
5. 関数ツリー
各関数は以下のような関係になる。
【】内の関数は直接呼び出すわけではなく、WDFイベントなどのコールバック関数の登録箇所となる。
WDF
├DriverEntry
│ 【xxxxEvtDeviceAdd】
│ 【xxxxEvtDriverContextCleanup】
├xxxxEvtDeviceAdd
│ └xxxxCreateDevice
│ ├xxxxQueueInitialize
│ │ ├【xxxxEvtIoDeviceControl】
│ │ └【xxxxEvtIoStop】
│ └【xxxxEvtDevicePrepareHardware】
├xxxxEvtDevicePrepareHardware
├xxxxEvtIoDeviceControl
├xxxxEvtIoStop
└xxxxEvtDriverContextCleanup
6. 関数
スケルトンで生成される各関数について説明する。
6.1 DriverEntry
ドライバーが初めて読み込まれるときに一度だけ実行する必要がある初期化処理を行う。
WDFのコールバック関数の登録をおこない、それぞれの関数を使用できるように準備する。
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status;
WDF_OBJECT_ATTRIBUTES attributes;
//
// WPP トレースの初期化
//
WPP_INIT_TRACING( DriverObject, RegistryPath );
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
//
// クリーンアップ コールバックを登録して、次の場合に WPP_CLEANUP を呼び出せるようにする
// フレームワーク ドライバー オブジェクトは、ドライバーのアンロード中に削除される
//
WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
attributes.EvtCleanupCallback = USBUMDF2EvtDriverContextCleanup;
WDF_DRIVER_CONFIG_INIT(&config,
USBUMDF2EvtDeviceAdd
);
status = WdfDriverCreate(DriverObject,
RegistryPath,
&attributes,
&config,
WDF_NO_HANDLE
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "WdfDriverCreate failed %!STATUS!", status);
WPP_CLEANUP(DriverObject);
return status;
}
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
6.2 EvtDeviceAdd
DriverEntryでコールバックのエントリポイントを登録する。
PnPマネージャーがデバイスの存在を報告するときに、デバイスの初期化操作を実行する。
初期化操作によって主に以下を作成する。
・デバイス オブジェクト
・I/O要求を受信時に使用するI/Oキュー
・アプリケーションがデバイスとの通信時に使用するデバイスのインターフェイス
・他のドライバからアクセスするためのデバイス固有のインターフェイス
NTSTATUS
xxxxEvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(Driver);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = USBUMDF2CreateDevice(DeviceInit);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
6.3 CreateDevice
EvtDeviceAddの子関数。
機能デバイス オブジェクト (FDO) または物理デバイスオブジェクト (PDO) を表すフレームワークデバイスオブジェクトを作成する。
- ドライバーが EvtDriverDeviceAdd コールバックからWDFDEVICE_INIT構造体を受け取った場合、FDOを作成する。
- ドライバーが EvtChildListCreateDevice コールバックまたは WdfPdoInitAllocate の呼び出しからWDFDEVICE_INIT構造体を受け取った場合、WdfDeviceCreate は PDO を作成する。
※ドライバーは、DeviceCreate を呼び出すと、WDFDEVICE_INIT構造体にアクセスできなくなる。
NTSTATUS
USBUMDF2CreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
WDF_OBJECT_ATTRIBUTES deviceAttributes;
PDEVICE_CONTEXT deviceContext;
WDFDEVICE device;
NTSTATUS status;
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = USBUMDF2EvtDevicePrepareHardware;
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (NT_SUCCESS(status)) {
//
// デバイス オブジェクトに関連付けたばかりのデバイス コンテキスト構造への
// ポインタを取得する
// DeviceGetContext は、WDF_DECLARE_CONTEXT_TYPE_WITH_NAME マクロを使用して
// 生成されるインライン関数である。
// この関数は型チェックを行い、デバイス コンテキストを返す。
// 間違ったオブジェクト ハンドルを渡すと、NULL が返され、
// フレームワーク検証モードで実行されている場合はアサートされる。
//
deviceContext = DeviceGetContext(device);
//
// コンテキストを初期化する
//
deviceContext->PrivateDeviceData = 0;
//
// アプリケーションがデバイスを見つけて通信できるように、
// デバイスインターフェイスを作成する。
//
status = WdfDeviceCreateDeviceInterface(
device,
&GUID_DEVINTERFACE_USBUMDF2,
NULL // ReferenceString
);
if (NT_SUCCESS(status)) {
//
// Initialize the I/O Package and any Queues
//
status = USBUMDF2QueueInitialize(device);
}
}
return status;
}
6.4 QueueInitialize
CreateDeviceの子関数。
呼び出すたびにデバイスの I/O キューを作成する。ドライバーはデバイスごとに複数の I/O キューを作成できる。
フレームワーク(WDF)は、親オブジェクトを削除するときにキュー オブジェクトを削除する。
NTSTATUS
xxxxQueueInitialize(
_In_ WDFDEVICE Device
)
{
WDFQUEUE queue;
NTSTATUS status;
WDF_IO_QUEUE_CONFIG queueConfig;
//
// WdfDeviceConfigureRequestDispatching を使用して
// 構成転送されない要求がここでディスパッチされるように、既定のキューを構成する
//
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
&queueConfig,
WdfIoQueueDispatchParallel
);
queueConfig.EvtIoDeviceControl = USBUMDF2EvtIoDeviceControl;
queueConfig.EvtIoStop = USBUMDF2EvtIoStop;
status = WdfIoQueueCreate(
Device,
&queueConfig,
WDF_NO_OBJECT_ATTRIBUTES,
&queue
);
if( !NT_SUCCESS(status) ) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "WdfIoQueueCreate failed %!STATUS!", status);
return status;
}
return status;
}
6.5 EvtDevicePrepareHardware
CreateDeviceでコールバックのエントリポイントを登録する。
デバイスの初期化処理を行う。
本スケルトンプログラムではUSBのコンフィギュレーションの設定などを行う。
必要に応じてデバイス固有の初期化処理もここで行う。
ドライバからデバイスにアクセスするために主に以下のような設定を行う。
・デバイスに割り当てられているメモリにアクセスできるように、物理メモリのアドレスを仮想アドレスにマップする
・デバイスのリビジョン番号を決める
・USB デバイスを構成する
・他のドライバーからデバイス固有のインターフェイスを取得する
NTSTATUS
xxxxEvtDevicePrepareHardware(
_In_ WDFDEVICE Device,
_In_ WDFCMRESLIST ResourceList,
_In_ WDFCMRESLIST ResourceListTranslated
)
{
NTSTATUS status;
PDEVICE_CONTEXT pDeviceContext;
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS configParams;
UNREFERENCED_PARAMETER(ResourceList);
UNREFERENCED_PARAMETER(ResourceListTranslated);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = STATUS_SUCCESS;
pDeviceContext = DeviceGetContext(Device);
//
// 基盤となる USB スタックと通信できるように、USB デバイス ハンドルを作成.
// WDFUSBDEVICE ハンドルはクエリに使用され、
// USB デバイスのすべての側面を構成および管理する。
// (デバイス プロパティ、バスプロパティ、 I/O の作成と同期を含む)
// PrepareHardware が最初に呼び出されたときにのみ、デバイスを作成する。
// デバイスがリソースのリバランスのために pnp マネージャーによって再起動される場合、
// 同じデバイス ハンドルを使用するが、再起動時に USB スタックがデバイスを
// 再構成する可能性があるため、インターフェイスを再度選択する。
//
if (pDeviceContext->UsbDevice == NULL) {
#if UMDF_VERSION_MINOR >= 25
WDF_USB_DEVICE_CREATE_CONFIG createParams;
WDF_USB_DEVICE_CREATE_CONFIG_INIT(&createParams,
USBD_CLIENT_CONTRACT_VERSION_602);
status = WdfUsbTargetDeviceCreateWithParameters(Device,
&createParams,
WDF_NO_OBJECT_ATTRIBUTES,
&pDeviceContext->UsbDevice
);
#else
status = WdfUsbTargetDeviceCreate(Device,
WDF_NO_OBJECT_ATTRIBUTES,
&pDeviceContext->UsbDevice
);
#endif
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,
"WdfUsbTargetDeviceCreateWithParameters failed 0x%x", status);
return status;
}
}
//
// 各インターフェースの最初の代替設定を使用して、デバイスの最初の構成を選択する
//
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES(&configParams,
0,
NULL
);
status = WdfUsbTargetDeviceSelectConfig(pDeviceContext->UsbDevice,
WDF_NO_OBJECT_ATTRIBUTES,
&configParams
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,
"WdfUsbTargetDeviceSelectConfig failed 0x%x", status);
return status;
}
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
6.6 EvtIoDeviceControl
QueueInitializeでコールバックのエントリポイントを登録する。
一般的にこのコールバック関数において、アプリケーションからの要求を解釈し、
指定されたデバイスの制御やデバイスからの情報の取得などを実装する。
※デバイスのI/O制御操作には、入力バッファー、出力バッファー、またはその両方が必要となる
VOID
xxxxEvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoControlCode
)
{
TraceEvents(TRACE_LEVEL_INFORMATION,
TRACE_QUEUE,
"%!FUNC! Queue 0x%p, Request 0x%p OutputBufferLength %d InputBufferLength %d IoControlCode %d",
Queue, Request, (int) OutputBufferLength, (int) InputBufferLength, IoControlCode);
WdfRequestComplete(Request, STATUS_SUCCESS);
return;
}
6.7 EvtIoStop
QueueInitializeでコールバックのエントリポイントを登録する。
指定された i/o 要求の処理を停止する。主に以下の動作を行うことができる。
・ドライバーが I/O 要求を所有している場合は、I/O 要求の完了または取り消し、要求の処理の延期を行う
・ドライバーが I/O 要求を I/O ターゲットに転送した場合は、要求のキャンセルを行うことができる。
※少量の時間内に完了することが保証されている要求に対して EvtIoStop でアクションを実行しない場合がある。
VOID
xxxxEvtIoStop(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ ULONG ActionFlags
)
{
TraceEvents(TRACE_LEVEL_INFORMATION,
TRACE_QUEUE,
"%!FUNC! Queue 0x%p, Request 0x%p ActionFlags %d",
Queue, Request, ActionFlags);
return;
}
6.8 EvtDriverContextCleanup
DriverEntryでコールバックのエントリポイントを登録する。
オブジェクトに対するドライバーの参照を削除することでオブジェクトを削除できるようにし、メモリやドライバオブジェクトを解放する。
VOID
xxxxEvtDriverContextCleanup(
_In_ WDFOBJECT DriverObject
)
{
UNREFERENCED_PARAMETER(DriverObject);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
//
// WPP トレースを停止する
//
WPP_CLEANUP( WdfDriverWdmGetDriverObject( (WDFDRIVER) DriverObject) );
}
7. 定数
7.1 GUID_DEVINTERFACE_xxxx
デバイスインターフェース用GUID。アプリケーションからアクセスする際に使用する。
DEFINE_GUID(GUID_DEVINTERFACE_xxxx※1,0xffff※2,0xffff,0xffff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff);
※1 xxxxにはプロジェクト名が入る。
※2 fにはGUID値が入る
8. 関連用語
・WDM(Windows Driver Model)
デバイスドライバのフレームワーク。
複雑な階層構造となっており、各ドライバはIRPを使って相互に通信を行う。
IRP_MJ_XXXというドライバの各機能を持つ関数コードを手動で実装する必要がある。
・WDF(Windows Driver Foundation)
WDMの後継として登場したデバイスドライバ開発のためのデバイスドライバモデル。
WDMではIRP_MJ_XXXとして実装しなければいけなかったものが、フレームワークで定められたコールバック関数を実装することで簡単に実装できるようになった。
・UMDF(User Mode Driver Framework)
WDFのフレームワークの一つ。
ユーザーモードのデバイスドライバ開発向けのフレームワーク。
・ユーザーモード
アプリケーションソフトやミドルウェアなど、主に利用者が直接触れるソフトウェアを動作させるための実行モード。
実行中のプログラムに何らかの瑕疵や問題があっても影響がシステム全体へ波及しないよう、
CPUの動作(メモリの管理など)に制限が加えられている。
9. おわりに
本調査の実施により、デバイスドライバに必要となる基本機能についてや、
デバイスドライバ開発をするうえで覚えておくべきコードの特徴についても学ぶことができました。
DriverEntryさえあればドライバとして動かすことができる(機能はなにもないが)、ということに驚きました。
開発を進めることでデバイスドライバに実装することができる機能と、
それに対応する関数の仕組みについても引き続き学んでいきたいと思いました。
デバイスドライバに関する基礎知識については弊社のデバドラ講座をお読みいただけると幸いです。
最後まで本記事をお読みいただきありがとうございました。
10. 参考・引用元URL
・https://qiita.com/spc_qiita/items/f7aea7a92b4f53106d3d
・https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/wdm-irps-and-kmdf-event-callback-functions
・https://learn.microsoft.com/ja-jp/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers
・https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfdevice/nf-wdfdevice-wdfdevicecreate
・https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdfdriver/nc-wdfdriver-evt_wdf_driver_device_add
・https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdfobject/nc-wdfobject-evt_wdf_object_context_cleanup
・https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdfdevice/nc-wdfdevice-evt_wdf_device_prepare_hardware
・https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfio/nf-wdfio-wdfioqueuecreate
・https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdfio/nc-wdfio-evt_wdf_io_queue_io_device_control
・https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfio/nc-wdfio-evt_wdf_io_queue_io_stop
・https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfio/ns-wdfio-_wdf_io_queue_config
・https://e-words.jp/w/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%A2%E3%83%BC%E3%83%89.html