アプリケーションからのI/O Controlをドライバで受け取る方法
皆さん、こんにちは。A寿です。
突然ですが、皆さんは象の背中に乗ったことはありますか?・・・このお話にご興味のある方は本文の最後の【閑話休題】までどうぞ。
さて、先日は、「アプリケーションからSCSIコマンドを発行する方法」として、I/O ControlでSCSIコマンドを発行する方法をご紹介いたしました。そのI/O Controlをドライバ側でどのように受け取ればよいのかが気になるところではないかと思います。また、特定のI/O ControlやSCSIコマンドを自分が開発しているドライバで受け取って、何らかの処理を加えたいと考える方もいらっしゃるかと思います。そこで、本日は、「アプリケーションからのI/O Controlをドライバで受け取る方法」をご案内したいと思います。
Storage Samples
https://msdn.microsoft.com/en-us/library/dd163407.aspx
のドキュメントに、たくさんのストレージ サンプル ドライバが紹介されておりますが、今回はディスククラスドライバの上位フィルタドライバのサンプルである、diskperfドライバを使って、前回のSPTIアプリケーションが発行したIOCTL_SCSI_PASS_THROUGHのI/O Controlを受け取って、そのI/O Controlと一緒に渡されるSCSI_PASS_THROUGH構造体の中身をデバッガに表示させてみたいと思います。表示結果を前回のブログと比較することにより、確かにアプリケーションからドライバにSCSIコマンドを渡せていることがわかると思います。
まず、一般的な、アプリケーションがI/O Controlを発行した場合に、それを受け取るドライバ側の処理を説明いたしますと、次の手順となります。
(1) ドライバのIRP_MJ_DEVICE_CONTROLのコールバック (*1) で、目的のI/O Controlが入ってきたかどうかを判断する
(2) 目的のI/O Controlが入ってきていたら、そのI/O Controlに対する処理を行う
(*1) コールバックとは、K里さんのブログ「DriverObject と DriverEntry」で「Dispatch Routine」と言われているものと同じです。
dispatch routineの言葉の定義は、例えば、
Windows Driver Kit: Glossary の D (https://msdn.microsoft.com/en-us/library/ms789534.aspx) の項目に
dispatch routine
An IRP-processing routine in a kernel-mode driver. Drivers export entry points for these routines through a dispatch table
in the DRIVER_OBJECT structure.
と載っています。
たったこれだけなので、非常に簡単です。しかも、ストレージの分野に限らず、I/O Controlを処理する必要があるカーネルドライバであれば、WDKのサンプルにたいてい(1)と(2)の処理が含まれていますので、これを真似することができます。
それでは、SPTIアプリケーションが、IOCTL_SCSI_PASS_THROUGHでMODE SENSEのSCSIコマンドをあるドライブレターを持つディスクに発行したとして、それを受け取るdiskperfドライバの動きを見ていきましょう。
(1) ドライバのIRP_MJ_DEVICE_CONTROLのコールバックで、目的のI/O Controlが入ってきたかどうかを判断する
diskperfドライバのサンプルは、
C:\WinDDK\6001.18002\src\storage\filters\diskperf
のフォルダにあります。diskperfドライバのサンプルの説明は、このフォルダ内のdiskperf.htmにもありますし、
DiskPerf Filter Driver
https://msdn.microsoft.com/en-us/library/dd163417.aspx
にもあります。このdiskperfフォルダのdiskperf.cにdiskperfドライバのソースコードのほぼ全てが含まれます。
ドライバのIRP_MJ_DEVICE_CONTROLのコールバックは、DriverEntry()から見つけます。diskperf.cの276行目から以下のようにDriverEntry()が始まっています。
276 NTSTATUS 277 DriverEntry( 278 IN PDRIVER_OBJECT DriverObject, 279 IN PUNICODE_STRING RegistryPath 280 ) |
そこからDriverEntry()の中を下へ進んでいきますと、
336 // 337 // Set up the device driver entry points. 338 // 339 340 DriverObject->MajorFunction[IRP_MJ_CREATE] = DiskPerfCreate; 341 DriverObject->MajorFunction[IRP_MJ_READ] = DiskPerfReadWrite; 342 DriverObject->MajorFunction[IRP_MJ_WRITE] = DiskPerfReadWrite; 343 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DiskPerfDeviceControl; 344 DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DiskPerfWmi; 345 346 DriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = DiskPerfShutdownFlush; 347 DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS] = DiskPerfShutdownFlush; 348 DriverObject->MajorFunction[IRP_MJ_PNP] = DiskPerfDispatchPnp; 349 DriverObject->MajorFunction[IRP_MJ_POWER] = DiskPerfDispatchPower; |
のように、ドライバのコールバックの登録をするために、DriverObjectのMajorFunctionを設定しているところが見つかります。IRP_MJ_DEVICE_CONTROLのコールバックは、上記の343行目のように、「DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = 」となっているところに代入している関数名を見れば、わかります。diskperfでは、DiskPerfDeviceControl()の関数、ということになります。
それでは、diskperf.cの1287行目から始まるDiskPerfDeviceControl()を見ていきましょう。
1287 NTSTATUS 1288 DiskPerfDeviceControl( 1289 PDEVICE_OBJECT DeviceObject, 1290 PIRP Irp 1291 ) |
このサンプルを見ますと、ドライバでのI/O Controlの一般的な処理がわかると思います。DiskPerfDeviceControl()そのものの処理の説明をしてしまうと本題から外れるので省略しますが、ドライバでのI/O Controlの一般的な処理としては、だいたい次の1-1.~1-3.のような処理をします。
1-1. 1st parameterが指しているDEVICE_OBJECT構造体から、 DEVICE_EXTENSION構造体を受け取ります。
DiskPerfDeviceControl()では、1314行目の処理に該当します。
1314 PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension; |
DEVICE_EXTENSION構造体とは、ドライバ開発者がデバイス単位に持たせたい固有の情報を定義するための構造体です。ドライバが管理するデバイスの情報は、DEVICE_OBJECT構造体に持たせるのですが、DEVICE_OBJECT構造体は開発者が定義を変更することはできません。これは、DEVICE_OBJECT構造体をドライバ間など様々なモジュールとやり取りする必要があるためです。diskperfドライバにも、このドライバ固有のDEVICE_EXTENSION構造体があり、diskperf.cの46行目から116行目にかけてその定義を見ることができます。またdiskperf内の様々な関数で、DEVICE_EXTENSION構造体のメンバ変数を変更する処理が行われていることがわかります。ここでは掲載しませんが、ご興味のある方はご覧ください。
1-2. 2nd parameterのIRP構造体へのポインタをIoGetCurrentIrpStackLocation() に入れて、 IO_STACK_LOCATION構造体を受け取ります。
DiskPerfDeviceControl()では、1315行目の処理に該当します。
1315 PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(Irp); |
この処理は、今回、非常に重要です。なぜなら、IO_STACK_LOCATION構造体に、アプリケーションから渡されたI/O Controlコードが含まれているからです。IO_STACK_LOCATION構造体の定義は、C:\WinDDK\6001.18002\inc\ddk\wdm.hにあります。(WDKをインストールされていない方は、
IO_STACK_LOCATION
https://msdn.microsoft.com/en-us/library/aa491675.aspx
でも確認できます。)
wdm.hの19145行目からIO_STACK_LOCATIONの定義が始まりますが、その中のParameters共用体の中にあるwdm.hの19328行目の構造体の中に、今回必要となる、IoControlCodeがあります。
19321 // 19322 // System service parameters for: NtDeviceIoControlFile 19323 // 19324 // Note that the user's output buffer is stored in the UserBuffer field 19325 // and the user's input buffer is stored in the SystemBuffer field. 19326 // 19327 19328 struct { 19329 ULONG OutputBufferLength; 19330 ULONG POINTER_ALIGNMENT InputBufferLength; 19331 ULONG POINTER_ALIGNMENT IoControlCode; 19332 PVOID Type3InputBuffer; 19333 } DeviceIoControl; |
1-3. IO_STACK_LOCATION構造体に含まれるI/O Controlコードを参照し、目的のI/O Controlなら、そのI/O Controlに対する処理を行います。
DiskPerfDeviceControl()では、1320行目以降の処理に該当します。
1320 if (currentIrpStack->Parameters.DeviceIoControl.IoControlCode == 1321 IOCTL_DISK_PERFORMANCE) { |
この関数の例では、IOCTL_DISK_PERFORMANCEのI/O Controlが来たら、処理を行うようになっています。1-2.でお話したように、IO_STACK_LOCATION構造体のParameters共用体のDeviceIoControl構造体のIoControlCodeを見ることでI/O Controlコードを知ることができることがおわかりになるかと思います。
以上の内容を踏まえ、当初の目的である、IOCTL_SCSI_PASS_THROUGHを区別するための処理を、このDiskPerfDeviceControl()に追加してみましょう。DiskPerfDeviceControl()では、これ以外のI/O Controlは全て1426行目のelse文以降で処理していますので、IOCTL_SCSI_PASS_THROUGHはここに入ってくることが考えられます。抜粋いたしますと、次の箇所です。
1426 else { 1427 1428 // 1429 // Set current stack back one. 1430 // 1431 1432 Irp->CurrentLocation++, 1433 Irp->Tail.Overlay.CurrentStackLocation++; 1434 1435 // 1436 // Pass unrecognized device control requests 1437 // down to next driver layer. 1438 // 1439 1440 return IoCallDriver(deviceExtension->TargetDeviceObject, Irp); 1441 } |
サンプルの処理は基本的に尊重するのがベターですので、今回はこのelseの中身をそのまま流用します。elseの中では、下位ドライバにIRPを渡すための処理を行っていますので、その処理を行う前の1428行目に、
if (currentIrpStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_SCSI_PASS_THROUGH) { } |
というコードを入れてみましょう。このif文により、IOCTL_SCSI_PASS_THROUGHが来た時の処理を、このif文の中で行うことができます。
(2) 目的のI/O Controlが入ってきていたら、そのI/O Controlに対する処理を行う
それでは、上記のif文の中で、IOCTL_SCSI_PASS_THROUGHの時の処理を記述しましょう。今回は、SPTIアプリケーションがIOCTL_SCSI_PASS_THROUGHと一緒に送ったSCSI_PASS_THROUGH構造体の中身をデバッガに表示させる、ということを目的にしています。SCSI_PASS_THROUGH構造体は、DiskPerfDeviceControl()の2nd parameterのIrpから受け取ることができます。より具体的には、Irp->AssociatedIrp.SystemBufferから受け取ることができます。
ここで、前回の「アプリケーションからSCSIコマンドを発行する方法」で示した、DeviceIoControl()関数を再度掲載します。
status = DeviceIoControl(fileHandle, IOCTL_SCSI_PASS_THROUGH, &sptwb, sizeof(SCSI_PASS_THROUGH), &sptwb, length, &returned, FALSE); |
この関数の3rd parameterが入力バッファのポインタ、4th parameterがそのサイズでした。上記のように、DeviceIoControl()関数の3rd,4th parameterを指定することで、SCSI_PASS_THROUGH構造体がIrp->AssociatedIrp.SystemBufferにコピーされるため、この構造体をカーネルドライバ側で受け取ることができます。
さて、受け取り方は簡単です。次のように、受け取りたい構造体のポインタに代入するだけです。その際、適切にキャストしておくのが安全です。
PSCSI_PASS_THROUGH spt = NULL; spt = (PSCSI_PASS_THROUGH) Irp->AssociatedIrp.SystemBuffer; |
これで、「spt->(メンバ変数)」のようにすれば、アプリケーションから受け取ったSCSI_PASS_THROUGH構造体のデータを参照することができます。
以上の内容から、diskperf.cの1428行目に、IOCTL_SCSI_PASS_THROUGHを受け取ってSCSI_PASS_THROUGH構造体のデータを表示するコードを追加すると、以下のようになります。
1426 else { 1427 1428 if (currentIrpStack->Parameters.DeviceIoControl.IoControlCode == 1429 IOCTL_SCSI_PASS_THROUGH) { 1430 PSCSI_PASS_THROUGH spt = NULL; 1431 1432 KdPrint(("DiskPerfDeviceControl: IOCTL_SCSI_PASS_THROUGH\n")); 1433 1434 spt = (PSCSI_PASS_THROUGH) Irp->AssociatedIrp.SystemBuffer; 1435 KdPrint(("spt->Length = %d\n", spt->Length)); 1436 KdPrint(("spt->PathId = %d\n", spt->PathId)); 1437 KdPrint(("spt->TargetId = %d\n", spt->TargetId)); 1438 KdPrint(("spt->Lun = %d\n", spt->Lun)); 1439 KdPrint(("spt->CdbLength = %d\n", spt->CdbLength)); 1440 KdPrint(("spt->SenseInfoLength = %d\n", spt->SenseInfoLength)); 1441 KdPrint(("spt->DataIn = %d\n", spt->DataIn)); 1442 KdPrint(("spt->DataTransferLength = %d\n", spt->DataTransferLength)); 1443 KdPrint(("spt->TimeOutValue = %d\n", spt->TimeOutValue)); 1444 KdPrint(("spt->DataBufferOffset = %d\n", spt->DataBufferOffset)); 1445 KdPrint(("spt->SenseInfoOffset = %d\n", spt->SenseInfoOffset)); 1446 KdPrint(("spt->Cdb[0] = %d\n", spt->Cdb[0])); 1447 KdPrint(("spt->Cdb[2] = %d\n", spt->Cdb[2])); 1448 KdPrint(("spt->Cdb[4] = %d\n", spt->Cdb[4])); 1449 } 1450 // 1451 // Set current stack back one. 1452 // 1453 1454 Irp->CurrentLocation++, 1455 Irp->Tail.Overlay.CurrentStackLocation++; 1456 1457 // 1458 // Pass unrecognized device control requests 1459 // down to next driver layer. 1460 // 1461 1462 return IoCallDriver(deviceExtension->TargetDeviceObject, Irp); 1463 } |
IOCTL_SCSI_PASS_THROUGHや、SCSI_PASS_THROUGH構造体の定義は、C:\WinDDK\6001.18002\inc\api\ntddscsi.hにありますので、
#include "ntddscsi.h" |
の行をdiskperf.cに追加しておきましょう。今回は、最後の#include文である、35行目の「#include "ntstrsafe.h"」の後に追加しました。
以上のようにコードを追加しましたら、diskperfドライバをビルドして、動作確認をしてみましょう。今回は、Vista x86のターゲットPC上にインストールして、かつ、デバッガにプリント文を出力させたいので、"Windows Vista and Windows Server 2008 x86 Checked Build Environment"のビルド環境を使って、ビルドしました。(ドライバのビルドについては、なおきお~さんの「ドライバのビルド方法」の記事を参考にしてください。)ビルドしますと、diskperf\objchk_wlh_x86\i386に、diskperf.sysというドライバファイルとdiskperf.pdbというシンボルファイルができます。
それではこのドライバをインストールしましょう。diskperf.htmの「Running the Sample」の項目を読みますと、diskperf.infを右クリックして、「インストール」を選べばインストールできると書いてあります。今回のテスト環境として、Windows Server 2008 R2 RCのHyper-V上に、Vista SP2 x86の仮想マシンを用意しました。このVista SP2 x86上のフォルダに、diskperf.sysとdiskperf.infをコピーします。そして、diskperf.infの56行目の[SourceDisksNames.x86]セクションに書いてあるとおり、diskperf.infをコピーしたフォルダにi386フォルダを作成し、そこにdiskperf.sysを入れます。(もしi386フォルダを作らずに、それ以外のフォルダにdiskperf.sysを置いた場合は、インストール時にdiskperf.sysの場所を指定するよう要求されます。)
54 ; WinXP and later 55 56 [SourceDisksNames.x86] 57 1 = %diskid1%,,,\i386 |
diskperf.infを右クリックしてインストールします。(この時、デバッグしていないので詳細は不明ですが、Vistaの環境によっては、「選択されたINFファイルでは、このインストールの方法はサポートされていません。」というインストールエラーのダイアログが出ることがあります。そのような場合には、[DefaultInstall.NT]を[DefaultInstall]に、[DefaultInstall.NT.Services]を[DefaultInstall.Services]に変更してみて試してみてください。)
diskperf.infをインストールすると、OSの再起動を求められますので、再起動します。OSが再起動した後、diskperf.infがインストールされたことをデバイスマネージャで確認してみます。以下の図のように、[ディスク ドライブ]のツリーにある[Virtual HD ATA Device]を右クリックし、[プロパティ]を開きます。
次に、[Virtual HD ATA Deviceのプロパティ]の[ドライバ]タブをクリックし、[ドライバの詳細]をクリックします。
[ドライバ ファイルの詳細]にdiskperf.sysがあることがわかります。
それでは、以前のブログ「Hyper-Vなどの仮想OSにwindbgをアタッチする方法」と同じ方法で、仮想マシンのVistaにWinDbgを接続しましょう。接続したら、WinDbgの[File]の[Symbol File Path...]にdiskperf.pdbのあるフォルダへのフルパス、[Source File Path...]にdiskperf.cのあるフォルダへのフルパスを指定してください。例えば、C:\develop\diskperf\objchk_wlh_x86\i386のフォルダにdiskperf.pdbがあるなら、[Symbol File Path...]には「C:\develop\diskperf\objchk_wlh_x86\i386」を指定します。C:\develop\diskperf\のフォルダにdiskperf.cがあるなら、[Source File Path...]に「C:\develop\diskperf\」を指定します。
WinDbgの設定が終わりましたら、せっかくですので、デバッガからdiskperf.sysがディスククラスドライバの上位フィルタドライバになっていることを確認してみましょう。まずは、WinDbgのBreakボタン( )をクリックし、ターゲットPCである仮想マシンのVistaの動きを止めましょう。Breakボタンをクリックすると、以下のような表示がCommandウィンドウに出力されます。
Break instruction exception - code 80000003 (first chance) ******************************************************************************* * * * You are seeing this message because you pressed either * * CTRL+C (if you run kd.exe) or, * * CTRL+BREAK (if you run WinDBG), * * on your debugger machine's keyboard. * * * * THIS IS NOT A BUG OR A SYSTEM CRASH * * * * If you did not intend to break into the debugger, press the "g" key, then * * press the "Enter" key now. This message might immediately reappear. If it * * does, press "g" and "Enter" again. * * * ******************************************************************************* nt!RtlpBreakWithStatusInstruction: 81906464 cc int 3 kd> |
次に、この前のK里さんの「DriverObject と DriverEntry」で説明がありましたように、!drvobjをdiskperf.sysに対して実行してみます。すると、以下のように表示されます。
kd> !drvobj diskperf Driver object (8440df38) is for: \Driver\diskperf Driver Extension List: (id , addr)
Device Object list: 844a72c8 |
この一番最後のDevice Object listに表示されている8桁の値(844a72c8)が、diskperf.sysが作成したデバイスオブジェクトへのアドレスです。今回は、仮想マシン上にOSの入っているディスク(VHD)が一つしかないので、リストにあるアドレスは一つとなっています。このアドレスを、!devstackの引数として実行しますと、以下のように、このデバイスオブジェクトが所属するデバイス スタックが表示されます。
kd> !devstack 844a72c8 !DevObj !DrvObj !DevExt ObjectName > 844a72c8 \Driver\diskperf 844a7380 844c67b8 \Driver\partmgr 844c6870 844c6ac8 \Driver\disk 844c6b80 DR0 8439d390 \Driver\storflt 843f84e0 83974848 \Driver\ACPI 842a2cb8 8395da40 \Driver\atapi 8395daf8 IdeDeviceP0T0L0-0 !DevNode 83972e30 : DeviceInst is "IDE\DiskVirtual_HD______________________________1.1.0___\5&35dc7040&0&0.0.0" ServiceName is "disk" |
確かに、ディスククラスドライバであるdisk.sysの上位に、diskperf.sysがあることがわかります。この方法を使っていただくと、自分がインストールしたドライバが、デバイススタックの正しい位置に入ったかどうかを確認できますので、機会があれば、ぜひお試しください。
ちょっと脱線しましたので、本題に戻ります。実は、ご存知の方もいらっしゃるかと思いますが、このまま仮想マシンのVista上でI/O Controlを発行しても、KdPrint()の表示内容はデバッガに出力されません。Vista以前のOSの場合であれば、Checked BuildするだけでKdPrint()の内容はデバッガに出力されましたが、Vistaからは、デバッガ上での設定またはターゲットPC側のレジストリ設定をする必要があります。そのようになった経緯や、設定の詳細などは、今回の本題ではないので、別の機会に譲りますが、興味のある方は、以下のドキュメントをご参照ください。
Reading and Filtering Debugging Messages
https://msdn.microsoft.com/en-us/library/ms792789.aspx
KdPrint()の内容をデバッガに出力するには、デバッガのCommandウィンドウ上で、
kd> ed nt!Kd_DEFAULT_Mask 0xf |
と実行します。ここまで終わりましたら、デバッガ上で行う作業は終了です。止めていたVistaを以下のコマンドで動かしましょう。
kd> g |
さて、それでは、いよいよ仮想マシンのVista上で、ディスクに対して、I/O Controlを発行してみましょう。まず、管理者権限でコマンドプロンプトを起動します。前回のブログ「アプリケーションからSCSIコマンドを発行する方法」で作成したspti.exeを、Vista上のあるフォルダ(今回はC:\sptiとします)にコピーします。コマンドプロンプト上で、C:\sptiに移動し、diskperf.sysが入っているディスク(今回はC:ドライブとします)に、
C:\spti> spti.exe c: |
を実行します。すると、SCSI_PASS_THROUGH構造体の内容が、デバッガのCommandウィンドウに以下のように出力されます。
DiskPerfDeviceControl: IOCTL_SCSI_PASS_THROUGH spt->Length = 44 spt->PathId = 0 spt->TargetId = 1 spt->Lun = 0 spt->CdbLength = 6 spt->SenseInfoLength = 32 spt->DataIn = 1 spt->DataTransferLength = 192 spt->TimeOutValue = 2 spt->DataBufferOffset = 80 spt->SenseInfoOffset = 48 spt->Cdb[0] = 26 spt->Cdb[2] = 63 spt->Cdb[4] = 192 |
出力されている値が、「アプリケーションからSCSIコマンドを発行する方法」で掲載した、SCSI_PASS_THROUGH構造体の値と同じであることがご確認いただけると思います。つまり、アプリケーションからIOCTL_SCSI_PASS_THROUGHとともに送ったSCIS_PASS_THROUGH構造体が、diskperfドライバに到達したことがおわかりいただけると思います。
――――――――――――――――
【閑話休題】皆さんは象の背中に乗ったことはありますか?
私は、千葉にある、象で有名な某動物園で乗ったことがあります。象に乗るまで、象の頭に髪が生えているということを知らなかったのですが、固そうな髪が意外とふさふさ(?)生えていて驚きました。(今、私の頭は象よりはふさふさですが、将来もそうありたいものだ、と思いました。)その動物園では5、6頭の象がショーをしてくれるのが圧巻でした。「こんなに象がいたら、食費が大変だ。 入場料だけでまかなえるのかな」と思っていたら、たくさんの子供さんたちに、象たちが象のぬいぐるみを1つ数千円で鼻で渡してあげていたり、象と一緒に写真をとったり、象にえさ(数百円の野菜)をあげたりするなどのサービスがあったので、「なるほど、お客様にいい思い出を作ってあげて、代わりに象たちがご飯を食べられる、いいシステムだなあ」と思いました。(発想が貧乏症ですみません。小学校時代、母親に「石焼き芋、買って」とお願いしたら「家で焼いた方が安いから、がまんしなさい」と言われた生い立ちがあるせいかもしれません。)小さいお子様のいらっしゃる方には、ぜひ思い出作りに行ってほしい場所です。(私は地方出身なので存じ上げませんが、もしかすると関東の方は皆さん行かれたことがあるのかもしれませんね。)