UMDF開発のすゝめ - 3
少しずつ書き進めてます。
チュートリアルドキュメント。
HelloWorld のソースコードまでできたので、
次はビルド方法です。
その他雑多なファイルを用意する
デバイスドライバの本体となるコードは実装し終わりましたが、ビルドするためにはいくつか細かいファイルを用意する必要があります。
簡単な説明とともに例を挙げていきます。
リソーススクリプト(.rcファイル)
DLLの説明やファイル名などを#defineしてからcommon.verをインクルードすると、その中で適宜設定してくれます。
#include <windows.h> #include <ntverp.h> #define VER_FILETYPE VFT_DLL #define VER_FILESUBTYPE VFT_UNKNOWN #define VER_FILEDESCRIPTION_STR "UMDF Sample DeviceDriver" #define VER_INTERNALNAME_STR "UmdfHello" #define VER_ORIGINALFILENAME_STR "UmdfHello.dll" #include "common.ver"
Exports.def ファイル
DLLファイルとして外部に公開しなければならない関数はDllGetClassObjectだけですのでEXPORTSしておきます。
ライブラリ名は先ほどリソーススクリプトで書いたものと統一します。
LIBRARY UmdfHello.dll EXPORTS DllGetClassObject PRIVATE
Sources
このファイルは先ほど!INCLUDEしたmakefile.defからさらに!INCLUDEされるファイルです。
標準で設定されない変数やリンクファイルなどの設定を行います。
ファイル名などは適宜設定します。
UMDF_VERSION=1 UMDF_MINOR_VERSION=5 TARGETTYPE=DYNLINK USE_MSVCRT=1 C_DEFINES=$(C_DEFINES) /D_UNICODE /DUNICODE TARGETLIBS=\ $(SDK_LIB_PATH)\strsafe.lib \ $(SDK_LIB_PATH)\kernel32.lib \ $(SDK_LIB_PATH)\advapi32.lib TARGETNAME=UmdfHello DLLDEF=Exports.def SOURCES=Hello.cpp Hello.rc
ビルドする
細かいファイルも全部揃ったらビルドします。
まずは
[Windows Driver Kits] -> [Build Environments] -> [Windows XP] -> [Windows XP x86 Checked Build Environment]
ここから環境設定済みのプロンプトを起動します。
Checked Buildはいわゆるデバッグバージョン、Free Buildはいわゆるリリースバージョンです。
次に、cdコマンドで先ほどのファイル一式がそろったフォルダへ移動し、buildと叩きます。
しばらくして
3 files compiled - 104 LPS 1 library built 1 executable built
このように完了メッセージがでてプロンプトに戻ってこればビルド完了です。objchk_wxp_x86\i386\UmdfHello.dllが生成されていれば成功です。
UMDF開発のすゝめ - 4
アーキテクチャに関する説明の部分。
デバイスドライバ
まず概念として理解してほしいことは、「Windowsはすべてのものを”オブジェクト”として抽象化している」ことです。例えば私たちは通常、ファイルやディレクトリはそういう”オブジェクト”があるものとして扱っていますが、実際にHDD内にそういう物体がありますか?ありませんよね。HDDにあるものはただのデータの羅列でしかなく、それをWindowsがオブジェクトとして「見せかけている」わけです。
もう1つ、コントロールパネルや一般のアプリケーションなどからデバイスとして見えるもの(例えばHDDやCOMなど)は、実際にはその機器ではなく「デバイスドライバと通信するための窓口」です。通常これをデバイスオブジェクトと呼びます。デバイスオブジェクト経由で要求を送ると、デバイスドライバがそれを解釈して間接的に実際のデバイスとやり取りを行うわけです。
「そんなに変わらないじゃん」と思われそうですが、この違いがこの先重要になるので強調しておきます。
さて、哲学はこのくらいにして実際にデバイスドライバがどのように動いているのか見てみましょう。
NTSTATUS DriverEntry( PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath ) { NTSTATUS NtStatus; /* 文字列構造体に "\Device\Exsample" という文字列を代入 */ UNICODE_STRING usDriverName; RtlInitUnicodeString( &usDriverName, L"\\Device\\Exsample" ); /* IoCreateDevice: デバイスオブジェクトを生成するカーネル用 API */ PDEVICE_OBJECT pDeviceObject = NULL; NtStatus = IoCreateDevice( pDriverObject, 0, &usDriverName, FILE_DEVICE_DISK, 0, FALSE, &pDeviceObject); if( NtStatus != STATUS_SUCCESS ) { /* エラー発生したのでリターン */ DbgPrint("IoCreateDevice Error!"); return NtStatus; } pDriverObject->MajorFunction[IRP_MJ_CREATE] = Example_Create; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = Example_Close; pDriverObject->MajorFunction[IRP_MJ_READ] = Example_Read; /* 省略 -- 他にも IRP_MJ_WRITE など */ return STATUS_SUCCESS; }
突然コード片を出しましたが、これは実際に動作する(カーネルモード)デバイスドライバのコード片です。
まず、Windowsカーネルがデバイスドライバを読み込むと、DriverEntry関数を呼びます。DLLの場合のDllMainに相当するものと考えてもらえれば大体間違いないです。
見たこともない構造体や関数があって最初は戸惑いますが、ここで行っていることは大きく分けて2つ。
・デバイスオブジェクトを生成する
・CREATE, CLOSE, READ, WRITE などの要求を受け取るコールバック関数を登録する
前者は簡単、カーネルモード用に用意されているIoCreateDevice APIをコールするだけでデバイスオブジェクトが作られます。
ここで作ったデバイスオブジェクトはユーザスペースからも見えていて、
HANDLE Device; Device = CreateFile( "\\Device\\Exsample", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL );
このように通常のWindows APIから操作が可能です。
後者に関してはもっと簡単で、引数として渡されるDRIVER_OBJECTにコールバック関数をガンガン代入するだけです。ユーザスペースから
DWORD dwReadSize;
ReadFile( Device, Buffer, ulBufferSize, &dwReadSize, NULL );
このようにREAD要求を発行すると、先ほど登録したExample_Read関数が呼ばれます。ちなみに本体はこんな感じです。
NTSTATUS Example_Read( PDEVICE_OBJECT DeviceObject, PIRP Irp ) { /* 読み込みに必要な処理を行う */ return STATUS_SUCCESS; }
一般的にデバイスドライバの説明と言えば、このあとさらにデータ受け渡しの方法やメモリマッピングの話に進むわけですが、これらは単にデバイスドライバの動作を実現するためのトリックであり、本質的にはすべてのデバイスドライバがこのような構造で動いています。今回、この章は単に基礎知識としてカーネルモードドライバを取り上げただけなので、これ以上に関しては割愛します。このあたりの話に興味を持たれた方はぜひMSDNなどで調べてみてください。