このテキストは「Windows Device Driver Programming Part 1」の続編という位置づけですので、読んでない方はまずそちらから先に読むことをお勧めします。また、このテキストは、著者自身も完全に理解して書いているわけではないので、間違った内容が多々あることをご了承ください。さらに、著者のスキルレベルの都合により、内容の分かりやすさをあまり重視できません。その辺りはごめんなさい。では、さっそくはじめましょう。
まずは以下のプログラムをダウンロードしてください。今回使用するプログラムです。
CUI環境でのプログラムと違い、GUI環境での一般的なWindowsプログラムは、OSからメッセージを受け取って処理を行います。WM_CREATEやWM_DESTROYといったメッセージをOSから受け取ったアプリケーションは、そのメッセージに対応した処理を実行するわけです。よって、MS-DOS環境でのCUIプログラムのように、プログラムを記述した順に(基本的に)上から実行されるわけではありません。「何かしらのユーザーの行動(クリックやキー入力など)に対して処理を行う」のがWindowsプログラムの概念でした。それと同じように、デバイスドライバも何かしらの動作に対して処理を行ないます。つまり、概念的には通常のWindowsアプリケーションと変わりません。
では、以下のプログラムを見てください。概念的には通常のWindowsアプリケーションと変わりませんが、記述方法や実際の仕組みは大きく違うので、その辺りを解説していきます。
----- ex1.cpp(DriverEntry関数)
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
PDEVICE_OBJECT pDeviceObject = NULL;
UNICODE_STRING usDriverName, usDosDeviceName;
MyDbgPrint("DriverEntry Called \r\n");
RtlInitUnicodeString(&usDriverName, L"\\Device\\Example");
RtlInitUnicodeString(&usDosDeviceName, L"\\DosDevices\\Example");
NTSTATUS NtStatus = IoCreateDevice(
pDriverObject, 0, &usDriverName, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceObject);
if(NtStatus != STATUS_SUCCESS){
MyDbgPrint("IoCreateDevice Error!\r\n");
return NtStatus;
}
for(int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
pDriverObject->MajorFunction[i] = Example_UnSupportedFunction;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = Example_Create;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = Example_Close;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Example_IoControl;
// ドライバをUnLoadするための関数
pDriverObject->DriverUnload = Example_Unload;
pDeviceObject->Flags |= 0;
pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);
return STATUS_SUCCESS;
}
-----
いろいろと小難しい構造体が定義されていますが、それらは後回しにします。まず一番重要なものは、「pDriverObject->MajorFunction」です。これは、デバイスドライバに何かしらの通知が送られた時に処理する関数を定義します。例えば、以下の部分を見てください。
----- ex1.cpp(DriverEntry関数)
for(int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
pDriverObject->MajorFunction[i] = Example_UnSupportedFunction;
-----
pDriverObject->MajorFunctionは配列となっており、配列の数はIRP_MJ_MAXIMUM_FUNCTION個あります。つまり、このプログラムは、すべての配列にExample_UnSupportedFunctionを設定していることになります。そして、ex1.cpp内のExample_UnSupportedFunction関数を見てください。
----- ex1.cpp(Example_UnSupportedFunction関数)
NTSTATUS Example_UnSupportedFunction(PDEVICE_OBJECT DeviceObject,
PIRP Irp)
{
MyDbgPrint("Example_UnSupportedFunction Called \r\n");
return STATUS_NOT_SUPPORTED;
}
-----
MyDbgPrint関数は、DbgPrint関数を少し自分流に改良し、printf形式で文字列を渡せるようにしたものです。ex1.cpp内で定義されています。Example_UnSupportedFunction関数は、その名の通り、このドライバではサポートしていない関数とします。よって、DbgViewには「Example_UnSupportedFunction Called \r\n」という文字列を出力するだけの関数となります。では、DriverEntry関数に戻ってください。
----- ex1.cpp(DriverEntry関数)
pDriverObject->MajorFunction[IRP_MJ_CREATE] = Example_Create;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = Example_Close;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Example_IoControl;
-----
上記のfor文に続いて、このようなコードがあります。「IRP_MJ_CREATE」はドライバが開かれた時、「IRP_MJ_CLOSE」はドライバが閉じられた時、「IRP_MJ_DEVICE_CONTROL」はI/Oコントロールが呼び出された時に実行される関数をそれぞれ指定します。
Example_Create関数とExample_Close関数は、ex1.cppにて定義されており、Example_UnSupportedFunction関数と同じような処理となっています。
----- ex1.cpp(Example_Create関数)
NTSTATUS Example_Create(PDEVICE_OBJECT DeviceObject,
PIRP Irp)
{
MyDbgPrint("Example_Create Called \r\n");
return STATUS_SUCCESS;
}
-----
----- ex1.cpp(Example_Close関数)
NTSTATUS Example_Close(PDEVICE_OBJECT DeviceObject,
PIRP Irp)
{
MyDbgPrint("Example_Close Called \r\n");
return STATUS_SUCCESS;
}
-----
最後のExample_IoControl関数の解説は置いておいて、先にDriverEntry関数とpDriverObject->DriverUnloadについて説明します。
pDriverObject->MajorFunctionとは、ぶっちゃけて言えばWindowsプログラムでのメッセージと同じようなものです。そして、pDriverObject->DriverUnloadも同じですが、pDriverObject->DriverUnloadはドライバがUnLoad(アンロード)される時に実行する処理を記述します。逆に、DriverEntry関数はドライバがLoad(ロード)される時に実行する処理ですね。つまり、Windowsプログラムでの、WM_CREATEメッセージとWM_DESTROYメッセージみたいなものです(おそらく)。そして、その他のメッセージがpDriverObject->MajorFunctionなわけです(多分)。ものすごく誤解を招きそうですが、そんな感じだと思います。
というわけで、UnLoad時の処理、Example_Unload関数を見てください。
----- ex1.cpp(Example_Unload関数)
VOID Example_Unload(IN PDRIVER_OBJECT DriverObject)
{
MyDbgPrint("Unload Called \r\n");
UNICODE_STRING usDosDeviceName;
RtlInitUnicodeString(&usDosDeviceName, L"\\DosDevices\\Example");
IoDeleteSymbolicLink(&usDosDeviceName);
IoDeleteDevice(DriverObject->DeviceObject);
}
-----
UNICODE_STRINGというよく分からん型と、同じくよく分からん関数が3つほどあります。これらの詳細はMSDNで調べてください(英語版のMSDNでないと検索できないようです)。
UNICODE_STRINGはその名の通り、UNICODE文字を扱う構造体でしょう。そしてRtlInitUnicodeString関数は、実際にその構造体に文字列を格納する関数だと思われます。つまり「\\DosDevices\\Example」という文字列がusDosDeviceName構造体に格納されたことになります。
そして、次のIoDeleteSymbolicLink関数は、DriverEntry内のIoCreateSymbolicLink関数と対応しています。それぞれがシンボリックリンクの削除と生成を行うわけですが、これらの関数は、ユーザーモードアプリケーションからのアクセスを許可する意味で使用されています。つまり、ドライバのLoad時(DriverEntry)にIoCreateSymbolicLinkを使いドライバへのシンボリックリンクを生成し、ドライバのUnLoad時(Example_Unload)にIoDeleteSymbolicLinkを使いドライバへのシンボリックリンクを削除しているわけです。
そして、最後のIoDeleteDevice関数ですが、これはデバイスドライバ自体を削除する関数であり、DriverEntry内のIoCreateDevice関数と対応しています。
デバイスドライバとユーザーモードアプリケーションが相互にデータを送受信するためには、I/Oコントロールを利用します。デバイスドライバ(ex1.cpp)側のExample_IoControl関数を解説する前にユーザーモードアプリケーション側のソースを示します。
----- ex1_user.cpp(抜粋)
int _cdecl main(void)
{
char szBuffer[1024] = "Hello from user mode!";
HANDLE hFile = CreateFile("\\\\.\\Example",
GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if(hFile == INVALID_HANDLE_VALUE){
perror("CreateFileが失敗しました");
return -1;
}
printf("UserModeMessage = '%s'\r\n", szBuffer);
DWORD dwReturn;
DeviceIoControl(hFile, IOCTL_ID_BUFFIO, szBuffer, lstrlen(szBuffer) + 1,
szBuffer, sizeof(szBuffer), &dwReturn, NULL);
printf("KernelModeMessage = '%s'\r\n", szBuffer);
CloseHandle(hFile);
return 0;
}
-----
デバイスドライバは、アプリケーション側からはファイルハンドルとして扱うことができます。よって、CreateFileを使ってデバイスドライバをオープンし、CloseHandleを使って閉じることができます。ファイルを扱うことと似ています。
デバイスドライバへはDeviceIoControlを使ってデータを送信します。szBufferには送信すべきデータを入れています。DeviceIoControlの第2引数には、ドライバ側とアプリケーション側の双方で同一のID(識別子)の指定する必要があります。よって、ヘッダファイルex1.hにてIOCTL_ID_BUFFIOを定義し、ex1.hをex1_user.cppとex1.cppの両方でincludeしています。
では、今度はアプリケーションからDeviceIoControlを利用して送信されてきたデータを処理するドライバ部分のソースを見ていきます。
----- ex1.cpp(Example_IoControl関数)
NTSTATUS Example_IoControl(PDEVICE_OBJECT DeviceObject,
PIRP Irp)
{
MyDbgPrint("Example_IoControl Called \r\n");
NTSTATUS NtStatus = STATUS_NOT_SUPPORTED;
PIO_STACK_LOCATION pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(!pIoStackIrp)
goto IOCTL_FUNC_END;
// アクセスIDを調べる
if(pIoStackIrp->Parameters.DeviceIoControl.IoControlCode != IOCTL_ID_BUFFIO)
goto IOCTL_FUNC_END;
// バッファポインタ取得
PCHAR pInputBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
PCHAR pOutputBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
if( ! (pInputBuffer && pOutputBuffer) )
goto IOCTL_FUNC_END;
// バッファサイズ取得
int InSize = pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength;
int OutSize = pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength;
int dwDataRead = 0, dwDataWritten = 0;
// 終端がNULLかどうかを調べる
BOOLEAN bFlag = FALSE;
for(int i=0; i < InSize && bFlag == FALSE; i++)
if(pInputBuffer[i] == '\0')
bFlag = TRUE;
if(bFlag == FALSE)
goto IOCTL_FUNC_END;
// ユーザーメッセージ出力
dwDataRead = strlen(pInputBuffer);
MyDbgPrint("UserModeMessage = '%s'\r\n", pInputBuffer);
// 返答メッセージをバッファへ
PCHAR pReturnData = "Hello from kernel mode!";
MyDbgPrint("KernelModeMessage = '%s'\r\n", pReturnData);
int dwDataSize = strlen(pReturnData);
if(OutSize < dwDataSize){
dwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}else{
dwDataWritten = dwDataSize;
RtlCopyMemory(pOutputBuffer, pReturnData, dwDataSize);
NtStatus = STATUS_SUCCESS;
}
IOCTL_FUNC_END:
Irp->IoStatus.Status = NtStatus;
Irp->IoStatus.Information = dwDataWritten;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return NtStatus;
}
-----
DeviceIoControl関数の第2引数に渡したID(識別子)のIOCTL_ID_BUFFIOは、ドライバ側では、pIoStackIrp->Parameters.DeviceIoControl.IoControlCodeに格納されています。よって最初にそれを調べます。次に入出力バッファのポインタとバッファサイズをそれぞれ取得し、入力バッファに文字列が存在するかどうかを調べます。そして、もし存在するならば、終端がNULLかどうかも調べます。
終端がNULLならば、MyDbgPrintを使って入力バッファのデータをDbgViewに出力し、続いて、RtlCopyMemory関数を使って、返答の文字列を出力バッファにコピーします。
まずはドライバ側(ex1.cpp)をコンパイルします。MAKEFILEファイルとSOURCESファイルを作成して、ビルドしてください。ドライバをビルドする詳しい方法はPart1を見てください。次にアプリケーション側(ex1_user.cpp)ですが、これは普通のアプリケーションを作成するのと同じで構いません。私はVC++.NETを使いましたが、どのようなコンパイラでも大丈夫でしょう。それぞれのビルドに成功すると、ex1.sysというシステムファイルと、ex1_user.exeというアプリケーションファイルが作成されます。
では、実行してみましょう。まずはデバッグ情報を確認したいため、DbgView.exeを起動してください。そして、Part1で使用した「Driver Install Program」を使って、ex1.sysをWindowsにインストールします。次に、コマンドプロンプトからex1_user.exeを実行します。最後に、再び「Driver Install Program」を使ってex1.sysをアンインストールしてドライバを終了します。


動作の流れとしては、最初にドライバが起動してDriverEntryが実行されます。そしてex1_user.exeが実行されてCreateFileにてドライバがオープンされたら、Example_Createが実行されます。そして、すぐにドライバへ「Hello from user mode!」を送ります。その時点でドライバの入力バッファには「Hello from user mode!」が存在することになり、同時にDeviceIoControlにて、ドライバ側のExample_IoControl関数が実行されます。Example_IoControlでは、入力バッファから文字列を取り出し、出力バッファに返答文字列をコピーします。そして、ex1_user.exeはその返答文字列を受け取り、コマンドプロンプトに出力して終了となります。ドライバはex1_user.exeでファイルハンドルがクローズされるのと同時にExample_Closeを実行します。そして、ドライバをアンインストールしたらExample_Unloadを呼び出してドライバも終了となります。
さて、いかがだったでしょうか。今回は「Windows Device Driver Programming Part 2」と題してお送りしましたが、実は、書き終えてものすごい不安に駆られました。というのも、この記事、ホントに読む人いるの? ってことなんですけどね(汗)。自分のサイトに公開する分にはまだ良いのですが、Wizard Bibleへの投稿記事だとやはりある程度読んでくれる人がいないとさびしいってのがホンネなので、マジでちょっと危機感が…。なので、以後この続きは自分のHP(http://ruffnex.oc.to/kenji/)でほそぼそとやっていくことにします(^^;。となると、来月用のネタをまた新しく探さなければならなくなるのですが、まぁなんとか頑張りますです。さて、最後になりましたが、ここまで読んでくれて本当にありがとうございます。
では、また会う日まで...