ほっしーの技術ネタ備忘録

技術ネタの備忘録です。基本的に私が忘れないためのものです。他の人の役にも立つといいなぁ。

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などで調べてみてください。