第2章 カーネルモードドライバ基礎編

2. まずはカーネルモードドライバを作る

2.1. カーネルモードドライバとは

カーネルモードドライバはWindowsのカーネル側で動作するドライバのことです。 使用できるAPIはユーザーモードのアプリケーションのように豊富ではなく、メモリ周辺の扱いも難しくなります(ページング可能かどうか、IRQLがどうなっているか等を考えないといけない)。 一方で、ハードウェアに関係するところをかなり自由に触れます。典型的なものとしてI/Oポートの操作が挙げられます。 WinNT系はハードウェアアクセスが厳しく制限されているイメージがあるかも知れませんが、カーネルドライバに限ってはやりたい放題出来ます。 特に事前申告することもなくI/Oポートへアクセスできます。

2.2. カーネルモードドライバのビルド環境

カーネルモードドライバのビルドにはWindows DDKが必要です。 また、基本的にVisual StudioのIDEを使わずにbuildコマンドでビルドするのが推奨のようです。 なお、ドライバを旧OSで使いたい場合は、旧OSに対応したDDKとVisual Studioが必要です。 特に旧OSのDDK(Win2kDDK等)はかつては無償配布されていましたが、今では入手に苦労するかも知れません。

このページではVisual Studio 6.0とWin2kDDKを使用することにします。 そんな古いもの動かないという時はエミュレータ環境を活用しましょう。

2.3. カーネルモードドライバのテンプレート

早速ですが、以下にI/Oポートを操作するカーネルモードドライバのテンプレートを示します。

サンプルファイル一式も用意しています。sampleio1.zip

【sample.c】

#include <ntddk.h>
#include "sample.h"

// アクセスするI/Oポート
#define SAMPLE_PORT    0x7E0

// デバイス名
#define SAMPLE_DEVNAME    L"\\Device\\SampleDevice"
// DOS名からアクセス可能なデバイス名 CreateFile等で\\.\SampleDeviceでアクセスできるようになる
#define SAMPLE_SYMNAME    L"\\DosDevices\\SampleDevice"

// 関数定義
NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT, PIRP);
VOID     DriverUnload(PDRIVER_OBJECT);
NTSTATUS CreateClose(PDEVICE_OBJECT, PIRP);

// ドライバのエントリポイント
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNICODE_STRING devName;
    UNICODE_STRING symLink;
    PDEVICE_OBJECT DeviceObject;

    // UnicodeStringを初期化 ドライバ系では基本的にUNICODE_STRINGでやりとり
    RtlInitUnicodeString(&devName, SAMPLE_DEVNAME);
    RtlInitUnicodeString(&symLink, SAMPLE_SYMNAME);

    // デバイスを作成
    IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    
    // DOS名からアクセス可能なデバイス名を登録(例:\DosDevices\AAAなら\\.\AAAで開けます)
    IoCreateSymbolicLink(&symLink, &devName);

    // ドライバへのリクエストを処理する関数を登録
    DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose; // CreateFileで作成したとき
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose; // ファイルを閉じたとき
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl; // DeviceIoControlを呼ばれたとき
    DriverObject->DriverUnload = DriverUnload; // ドライバのアンロード時に呼ばれる

    return STATUS_SUCCESS;
}

NTSTATUS CreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    // ファイル単位で動作を変える必要はないので、何もせずに成功とする
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING symLink = {0};
    
    // UnicodeStringを初期化
    RtlInitUnicodeString(&symLink, SAMPLE_SYMNAME);

    // DOS名からアクセス可能なデバイス名を登録解除
    IoDeleteSymbolicLink(&symLink);
    
    // デバイスを削除
    IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp); // IRPに付属する情報取得
    NTSTATUS status = STATUS_SUCCESS;

    // DeviceIoControlに渡されるIoControlCodeで分岐
    switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
    {
        case IOCTL_SAMPLE_READ:
        {
            // I/Oポート読み取り
            ULONG bufferLen = irpSp->Parameters.DeviceIoControl.InputBufferLength;
            PIOPORT_SAMPLE_DATA ioData = (PIOPORT_SAMPLE_DATA)Irp->AssociatedIrp.SystemBuffer;
            
            // バッファ長さが想定された長さでないときはエラー
            if (bufferLen != sizeof(IOPORT_SAMPLE_DATA)) {
                status = STATUS_INVALID_DEVICE_REQUEST;
                break;
            }
            
            // I/Oポート読み取り ポート番号は本来ポインタではないがPUCHAR型の数値として指定
            ioData->data = READ_PORT_UCHAR((PUCHAR)SAMPLE_PORT);
            
            // 戻すデータのサイズ
            Irp->IoStatus.Information = sizeof(IOPORT_SAMPLE_DATA);
            
            break;
        }
        case IOCTL_SAMPLE_WRITE:
        {
            // I/Oポート書き込み
            ULONG bufferLen = irpSp->Parameters.DeviceIoControl.InputBufferLength;
            PIOPORT_SAMPLE_DATA ioData = (PIOPORT_SAMPLE_DATA)Irp->AssociatedIrp.SystemBuffer;
            
            // バッファ長さが想定された長さでないときはエラー
            if (bufferLen != sizeof(IOPORT_SAMPLE_DATA)) {
                status = STATUS_INVALID_DEVICE_REQUEST;
                break;
            }
            
            // I/Oポート書き込み ポート番号は本来ポインタではないがPUCHAR型の数値として指定
            WRITE_PORT_UCHAR((PUCHAR)SAMPLE_PORT, (UCHAR)ioData->data);
            
            // 未使用
            Irp->IoStatus.Information = 0;
            
            break;
        }
        default:
            // 無効なリクエスト
            status = STATUS_INVALID_DEVICE_REQUEST;
            break;
    }

    // ステータスを返す
    Irp->IoStatus.Status = status;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}
【sample.h】

#define IOCTL_SAMPLE_READ \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_SAMPLE_WRITE \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef struct _IOPORT_SAMPLE_DATA {
    ULONG  data;
} IOPORT_SAMPLE_DATA, *PIOPORT_SAMPLE_DATA;

ビルドするためには以下の拡張子無しのファイルも必要ですが、ほぼテンプレートのままでOKです。

【makefile】

!INCLUDE $(NTMAKEENV)\makefile.def
【sources】

TARGETNAME=sample
TARGETTYPE=DRIVER
TARGETPATH=obj
SOURCES= \
    sample.c

makefileについては共通ですので、あえて変更する必要はありません。 このまま使用してください。

sourcesについてはTARGETNAMEに出力するsysファイルの名前を、TARGETTYPEにDRIVERを、TARGETPATHにobjを指定します。 SOURCESにはソースファイルを指定します。改行するときは行の最後に"\"を付けてください。

Win2kDDKの場合、ビルドは上記ファイル群が入ったディレクトリでbuildコマンドを実行することで行えます。 結果はfreeビルド(デバッグ無しRelease用)なら通常はobjfre\i386に出力されると思います。

出力先ディレクトリがないというエラーが出るかも知れません。 その場合は出力先ディレクトリ(例えばobjfre\i386)を手動で作成してください。

2.4. カーネルモードドライバの登録

ドライバの登録は色々な方法で出来ますが、例えば次のようなレジストリキーを登録すれば可能です。 削除する場合はこのレジストリキーを丸ごと削除すればOKです。


HKLM,"System\CurrentControlSet\Services\SampleDevice","Type",0x00010001,1
HKLM,"System\CurrentControlSet\Services\SampleDevice","Start",0x00010001,3
HKLM,"System\CurrentControlSet\Services\SampleDevice","ErrorControl",0x00010001,1
HKLM,"System\CurrentControlSet\Services\SampleDevice","ImagePath",0x00020000,"system32\drivers\sample.sys"
HKLM,"System\CurrentControlSet\Services\SampleDevice","DisplayName",0x00000000,"Sample Driver"

登録したドライバを動かすには、ビルドしたドライバファイル「sample.sys」をsystem32\drivers\へコピーし、システムを再起動して下さい。 再起動した後、net start SampleDevice(レジストリキーの名前)で起動できます。

自動起動にしたい場合はレジストリの"Start"を2にすれば可能ですが、カーネルモードで例外が出るとブルースクリーンのリスクがあるので、十分にテストしてからにしてください。

infファイルで登録したい場合は以下のような記述になります。 レガシードライバのため、インストールはデバイスの追加ウィザードではなく、infファイル右クリック→インストールで行う必要があります。


[Version]
Signature="$Windows NT$"
Class=LegacyDriver
Provider=%ProviderName%
DriverVer=05/20/2025,1.0.0.0

[DestinationDirs]
DefaultDestDir = 12 ; system32\drivers

[DefaultInstall]
CopyFiles=Sample.Copy
AddReg=Sample.AddReg

[Sample.Copy]
sample.sys

[Sample.AddReg]
HKLM,"System\CurrentControlSet\Services\SampleDevice","Type",0x00010001,1
HKLM,"System\CurrentControlSet\Services\SampleDevice","Start",0x00010001,3
HKLM,"System\CurrentControlSet\Services\SampleDevice","ErrorControl",0x00010001,1
HKLM,"System\CurrentControlSet\Services\SampleDevice","ImagePath",0x00020000,"system32\drivers\sample.sys"
HKLM,"System\CurrentControlSet\Services\SampleDevice","DisplayName",0x00000000,"Sample Driver"

[SourceDisksNames]
1=%DiskName%,,,

[SourceDisksFiles]
sample.sys=1

[Strings]
ProviderName="My Driver"
MfgName="My Driver"
NP2HostDrive.DeviceDesc="サンプルデバイスドライバ"
DiskName="サンプルデバイスドライバ インストールディスク"

2.5. カーネルモードドライバとの通信

先に紹介したサンプルドライバは以下のようなプログラムでユーザーモードアプリケーションと通信できます。

CreateFileでドライバのIRP_MJ_CREATEが、CloseHandleでドライバのIRP_MJ_CLOSEが呼ばれます。 つまりCreateFile毎にドライバは個別の処理を走らせることが可能ですが、ここではまだ扱いません。

DeviceIoControlでドライバのIRP_MJ_DEVICE_CONTROLが呼ばれます。 自作の汎用ドライバでは基本的にDeviceIoControlを使用して通信することになります。

【usermode.c】

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "sample.h"

int main(int argc, char const *argv[]) 
{
    IOPORT_SAMPLE_DATA ioData = {0};
    DWORD returned;
    HANDLE hDevice;

    // パラメータセット
    ioData.data = 39; // 出力する値

    // デバイスオープン
    hDevice = CreateFile("\\\\.\\SampleDevice", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (hDevice == INVALID_HANDLE_VALUE) 
    {
        printf("Error: ドライバに接続できません。\n");
        return 1;
    }

    // コマンド送信
    // Write
    if (DeviceIoControl(hDevice, IOCTL_SAMPLE_WRITE, &ioData, sizeof(ioData), &ioData, sizeof(ioData), &returned, NULL))
    {
        printf("WRITE SUCCESS: %d\n", ioData.data);
    }
    else
    {
        printf("Error: ポートWRITEアクセスに失敗しました。\n");
        CloseHandle(hDevice);
        return 1;
    }
    // Read
    if (DeviceIoControl(hDevice, IOCTL_SAMPLE_READ, &ioData, sizeof(ioData), &ioData, sizeof(ioData), &returned, NULL))
    {
        printf("READ SUCCESS: %d\n", ioData.data);
    }
    else
    {
        printf("Error: ポートREADアクセスに失敗しました。\n");
        CloseHandle(hDevice);
        return 1;
    }
    CloseHandle(hDevice);
    
    return 0;
}

このサンプルは単純なので、cl usermode.cだけでコンパイルできます。

試す場合は先のドライバで指定したI/Oポートに何もいないことを確認してください。 何かいる場合、そのデバイスにI/Oを送ることになるので何が起こるか分かりません。 何もいない場合、READ時に常に全ビット1(今回の場合0xffつまり255)が返ってくるはずです。

2.6. 詳細説明

2.6.1. 定数定義


#define SAMPLE_PORT    0x7E0
#define SAMPLE_DEVNAME    L"\\Device\\SampleDevice"
#define SAMPLE_SYMNAME    L"\\DosDevices\\SampleDevice"

2.6.2. DriverEntry 関数(ドライバの初期化)


NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)

ドライバが読み込まれると最初に呼ばれる関数です。

デバイスを IoCreateDevice で作成し、シンボリックリンクを張ります。 また、IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_DEVICE_CONTROL に対して処理関数を設定しています。 ドライバのアンロード処理(DriverUnload)もここで登録します。

2.6.3. CreateClose 関数(ファイルオープン/クローズ処理)


NTSTATUS CreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)

デバイスファイルが開かれたとき(CreateFile)や閉じられたとき(CloseHandle)に呼ばれます。今回は特にファイル毎の処理が必要ないため、成功を返すだけにしています。

2.6.4. DriverUnload 関数(ドライバのアンロード)


VOID DriverUnload(PDRIVER_OBJECT DriverObject)

ドライバがアンロードされるときに呼ばれます。 シンボリックリンクとデバイスオブジェクトの削除を行います。

2.6.5. DispatchDeviceControl 関数(I/O制御コード処理)


NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)

ユーザーモードアプリが DeviceIoControl を呼んだときに実行されます。 IoControlCode に応じて処理を分岐しています。

IOCTL_SAMPLE_READはI/Oポートから1バイト読み取ります。


ioData->data = READ_PORT_UCHAR((PUCHAR)SAMPLE_PORT);

IOCTL_SAMPLE_WRITEはI/Oポートへ1バイト書き込みます。


WRITE_PORT_UCHAR((PUCHAR)SAMPLE_PORT, (UCHAR)ioData->data);

【注意】ポート番号は本来ポインタではありませんが、PUCHAR型の数値として指定する必要があります。 つまり、UCHAR型変数のポインタという意味ではないので、以下のような書き方は間違いです。 このように書くと、UCHAR型変数のアドレスをI/Oポート番号扱いしてアクセスしてしまいます(そこに何もいなければ暴走しませんがロシアンルーレット状態です)。


// アクセスするI/Oポート
#define SAMPLE_PORT    0x7E0

// 間違った書き方!! 変数portが指すアドレスをI/Oポート番号としてアクセスしてしまう。
UCHAR port = SAMPLE_PORT;
WRITE_PORT_UCHAR(&port, (UCHAR)ioData->data);

// 正しい書き方 変数のポインタとしては異常だがこれが正解
PUCHAR port = (PUCHAR)SAMPLE_PORT;
WRITE_PORT_UCHAR(port, (UCHAR)ioData->data);

2.6.6. CTL_CODE マクロによる IOCTL 定義


#define IOCTL_SAMPLE_READ \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_SAMPLE_WRITE \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

CTL_CODE マクロは、ユーザーモードアプリケーションとカーネルモードドライバ間の命令(IOCTLコード)を定義するためのものです。

パラメータ構成:CTL_CODE(DeviceType, Function, Method, Access)

このIOCTLコードは DeviceIoControl API に渡され、ドライバ側の DispatchDeviceControl() で処理されます。

2.6.7. 通信のためのIOPORT_SAMPLE_DATA構造体


typedef struct _IOPORT_SAMPLE_DATA {
    ULONG data;
} IOPORT_SAMPLE_DATA, *PIOPORT_SAMPLE_DATA;

I/Oポートへの読み書き対象のデータ(1個)を保持する構造体です。 ULONG は 32ビット整数ですが、実際には READ_PORT_UCHAR / WRITE_PORT_UCHAR は 8ビット(UCHAR)のみ扱うため、上位ビットは無視されます。

2.6.8. IRP処理

DriverObject->MajorFunctionに割り当てたすべての関数で IRP(I/O要求パケット)を処理し、IoCompleteRequest を呼んで完了させる必要があります。 完了させ忘れた場合、ドライバ内で正常に完了していないIRPが溜まり、システムに不具合を起こします。

IRP_MJ_DEVICE_CONTROLの場合、Irp->IoStatus.Informationには出力データ(呼び出し元へ返すデータ)のサイズを入れます。 他の場合でも大体似たような感じですが、たまに違う意味の値(ステータスの補足情報等)を入れる場合もあります。 仕様を確認するようにしてください。


    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = dataLength;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

なお、IRPの処理中にIoMarkIrpPendingで保留状態にしてからSTATUS_PENDINGを返し、IoCompleteRequestを後のタイミングで行う場合もあります。 IoCompleteRequestを任意のタイミングで送ることで、OSへのイベント通知のような挙動が実現されます。 詳細はファイルシステムドライバの所で説明します。

※IoCompleteRequestを呼んだ後のIrpは無効です。 IoCompleteRequestの後でうっかりreturn Irp->IoStatus.Status;等としないように注意。

2.6.9. 入出力バッファ

ドライバとのデータのやりとりは原則としてバッファを経由して行います。 このバッファには以下の3方式があります。

簡単にドライバを作りたい場合は、METHOD_BUFFERED一択です。 ただし、今回のテーマであるファイルシステムドライバのようにOSと連携する場合には、他の方法を求めてくることがあります。 対応できるようにはしておきましょう。

2.6.10. 動的メモリ割り当て

上のサンプルにはありませんが、重要なのでこちらに記載しておきます。

カーネルモードでのメモリ割り当てはnewやmallocではなくExAllocatePoolを使用します。 これは以下のようにして使用します。


    // ページングしない領域にメモリ確保
    LONG *lpTest = (LONG*)ExAllocatePool(NonPagedPool, 割り当てるサイズ);
    if(!lpTest){
        // 割り当て失敗
        return;
    }
    
    // 確保したメモリで何か処理
    
    // メモリ解放
    ExFreePool(lpTest);

カーネルモードドライバの特異な点としては、割り当て時にどこにメモリを割り当てるかを指定できることがあります。 指定できるものは多数ありますが、主にNonPagedPoolPagedPoolのどちらかになります。 どちらを使うべきかについては、割り当てが小サイズで面倒な目に遭いたくなければNonPagedPoolにしてください。

何故かが気になる方のために大雑把に解説すると、以下のようになります。

問題の後者は、ユーザーモードで普通に使われるメモリ割り当て方法です。 いわゆる仮想メモリとして補助記憶装置をメインメモリの拡張として使うことができ、メモリが少ない環境でも大量のメモリを消費するプログラムを動かせます。

この方法はユーザーモードでは仮想メモリでディスクがゴリゴリ動いて遅くなるくらいの影響しかありませんが、カーネルモードではかなり面倒な問題を引き起こします。 それは、カーネルモードではページイン/ページアウト処理が出来ない実行状態(IRQL==PASSIVE_LEVELではない)があるためです。 この状態で必要なデータがページアウトしていた場合(ページフォールトが発生した場合)、即ブルースクリーンになります。


LONG *g_lpTest;

VOID init(){
    // ページング可能な領域に30byteメモリ確保
    g_lpTest = (LONG*)ExAllocatePool(PagedPool, 30);
}

VOID process(){
    // 様々な処理によって、どこかでg_lpTestが指すメモリ領域がページアウトしたとする
    // IRQLはページング不能な状態(IRQL==PASSIVE_LEVELではない)とする
    g_lpTest[0] = 1; // ページフォールトでブルースクリーン発生!
}

つまり、PagedPoolを使う場合は今の実行状態(IRQL)がどうなっているのかをきっちり把握しておかなければなりません。 また、自分だけが気をつけておけば良いのではなく、引数などでそれを渡す場合には、相手方がどのIRQLで処理するのかも把握しておく必要があります。 なお、ページアウトは常に発生するわけではないので、確率でブルースクリーンのようなデバッグが難しい嫌な挙動になります。

そういうわけで、特別メモリを節約したい状況でなければNonPagedPoolにするのをおすすめします。

※グローバル変数や関数内のローカル変数などはNonPagedPoolの場合と同じくページアウトしないので、安心してアクセスできます。

※ExAllocatePoolWithTagというものもあり、第3引数として4文字の任意のタグを受け取れます。 デバッグ時のメモリリークの特定などに活用できるそうです。

2.6.11. 排他制御

これも上のサンプルにはありませんが、やはり重要なのでこちらに記載しておきます。

WinAPIで排他制御といえばEnterCriticalSection/LeaveCriticalSectionが手軽ですが、カーネルモードではそうはいきません。 特にややこしいのは、先に紹介した割り込み要求レベル(IRQL)の値によって利用可能なものが変わってくることです。 一般にはIRQLが高くなると制約が大きくなっていきます。 そのあたりも含めて解説していきます。

カーネルモードでの排他制御は何種類かありますが、少なくともWinNT3.5以降で使えるものとしてはSpinLock系とMutex系があります。

SpinLock系

SpinLock系(KeAcquireSpinLock, KeReleaseSpinLock等)は無限ループ待機系でCPUを占有しますが、IRQL=DISPATCH_LEVEL,APC_LEVEL,PASSIVE_LEVELの3つで使用できます。ただし、排他領域内はIRQL=DISPATCH_LEVELで動作することになるため、注意が必要です。

まず、IRQL=DISPATCH_LEVELではスレッドの切り替えやページング処理が出来ません。 仮想メモリでページアウトされている領域にアクセスするとブルースクリーンになります。 つまり、PagedPoolで確保されたメモリへのアクセスは地雷原です。

また、IRQL=DISPATCH_LEVELでは実行できない関数も多数あります。 関数がIRQL=DISPATCH_LEVELで実行可能かを確かめておく必要があります。

さらに、EnterCriticalSection/LeaveCriticalSectionでは可能なロックへの再入が出来ません。 つまり、SpinLockをReleaseせずに2回連続で呼ぶとアウトです。

Mutex系

Mutex系(Mutex, FastMutex等)は基本的にユーザーモードと同じく、スレッドの停止で待機を行います。 したがって、スレッドの切り替えができるIRQL=PASSIVE_LEVEL,APC_LEVELで使用可能です。

排他処理実行中はIRQL=PASSIVE_LEVELなのかAPC_LEVELなのかよく分かっていません(おいっ)。

なおMutex系もロックへの再入が出来ません。 つまり、排他領域へReleaseせずに2回連続入ろうとするとアウトです。

2.7. デバッグログを確認する

デバッグ時には何らかのログを出力して動作を追うというのが一般的です。 カーネルモードドライバでも同じようにデバッグログの出力を行うことが出来ます。

2.7.1. デバッグログの出力方法

カーネルモードドライバではログ出力のためにKdPrintというマクロが用意されています。 なお、このマクロはcheckedビルド(デバッグ用ビルド)の場合のみ有効です。 freeビルド(リリース用ビルド)では何も出力されませんので注意してください。

KdPrintの使い方は簡単です。 以下のようにすると「Debug Log!!!!」というテキストをログ出力します。 なお、二重括弧になっているのはKdPrintの仕様です。 要らないと思って間違って消さないようにしてください。


KdPrint(("Debug Log!!!!\n"));

KdPrintマクロはC言語のprintf関数のように書式指定が可能です。 例えば、次のようにすると、変数statusの内容を表示できます。


KdPrint(("IoCreateSymbolicLink failed: STATUS=0x%08x\n", status));

追加の注意事項として、現在の割り込み要求レベル(IRQL)によっては使えない書式があります。 具体的には、数値の表示程度なら大抵の場合出来ますが、文字列の表示系にはIRQL制限があったりします。

※freeビルド(リリース用ビルド)でもログを出力したい場合、KdPrintマクロが呼び出しているDbgPrint関数を直接呼び出すと出力できます。

2.7.2. デバッグログの表示方法

カーネルモードドライバで出力されたログは通常の方法では見えないため、それ用の特別な準備が要ります。 大きく分けると以下の2つの方法があります。

  1. WinDbgを使用してシリアルポートを経由して別PCでログ確認する方法
  2. DebugViewを使用してドライバを動かしているOS上でログを確認する方法

1の方法はブート時から使用可能でカーネルモードデバッグとしては正統な気もしますが、今更シリアルポートが使える環境を用意するのが大変な点、他の代替の方法が上手く動かなくて苦労する、等があるのでやや敷居が高いです。

2の方法は先にOSを起動しないといけない点でブートデバイスなどのデバッグは難しくなります(一応メニューからブート時のログを取る機能がありますので、ブルースクリーンにさえしなければログを見ることは可能です)。 しかし、今回作成しようとしているファイルシステムドライバはnet start/net stopでOS起動後に開始と停止が出来ますので、実質的には困りません。使い方は簡単で、DebugViewを起動してCaptureメニュー→Capture KernelをONにしておくだけです。

そういうことですので、今回のファイルシステムドライバ製作にあたっては2の方法を推奨します。 なお、旧OSで動かしたい場合は古いバージョンのDebugViewが必要です。 今となっては正式に配布されていませんが、かつてはhttp://www.sysinternals.com/ntw2k/freeware/debugview.shtmlで配布されていたとだけ書いておきます。

2.8. 【Coffee Break】バージョン情報を付ける

上記で作ったドライバはバージョン情報も何もついていない傍目には怪しげなファイルです。 別になくても害はないのですが、雰囲気が出ますのでバージョン情報を付けてみましょう。

サンプルファイル一式も用意しています。sampleio2.zip

まずは以下のファイルを作成します。 普通のWin32リソースファイルです。

【sample.rc】

#include <winver.h>

VS_VERSION_INFO VERSIONINFO
 FILEVERSION    0,1,0,0
 PRODUCTVERSION 0,1,0,0
 FILEFLAGSMASK  0x3fL
 FILEFLAGS      0x0L
 FILEOS         VOS_NT
 FILETYPE       VFT_DRV
 FILESUBTYPE    VFT2_DRV_SYSTEM
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "041103a4"
        BEGIN
            VALUE "CompanyName",      "\0"
            VALUE "FileDescription",  "サンプルドライバ(I/Oポートアクセス)\0"
            VALUE "FileVersion",      "0.1.0.0\0"
            VALUE "InternalName",     "sample.sys\0"
            VALUE "OriginalFilename", "sample.sys\0"
            VALUE "ProductName",      "Sample Driver\0"
            VALUE "ProductVersion",   "0.1.0.0\0"
            VALUE "LegalCopyright",   "\0"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x0411, 932
    END
END

作成できたら、sourcesに追加しましょう。

【sources】

TARGETNAME=sample
TARGETTYPE=DRIVER
TARGETPATH=obj
SOURCES= \
    sample.c \
    sample.rc

これでビルドすればバージョン情報付きのsysファイルが出来上がります。

第3章 ファイルシステムドライバ製作編① へ移動

最小構成の仮想ファイルシステムドライバの製作に戻る

資料集に戻る

トップに戻る