Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

Simple WDM LoopBack Driver

4.91/5 (24 votes)
11 Mar 2009GPL38 min read 62.6K   2.5K  
This article is for developers who are writing Windows kernel device drivers for the first time and want to experiment with a simple example with source code.

Introduction

There are many articles available on the net on Windows kernel device drivers. I want to add another article to the list. This article is for those who want to start with a simple Windows kernel driver example. This driver example does not need any hardware to install into the Windows system. It is a pseudo example, a loop back driver. The application writes whatever data is available to the driver, and the data will be returned when the application reads from the driver.

Background

This article is for developers who are writing Windows kernel device drivers for the first time and want to experiment with a simple example with source code. If you have developed device drivers in a different OS, then it might be one of the best examples to start with.

Strong “C” language skills are required to understand the concepts better.

Introduction to WDM

Every driver starts with a DriverEntry function in Windows. DriverEntry, as its name suggests, is the driver entry point. In the DriverEntry implementation, the WDM object has to be instantiated, and we also provide WDM with the callbacks to the device driver. The driver shows how to process reads and writes.

The kernel usually runs the code in the driver by sending I/O request packets (IRPs). For example, a Win32 ReadFile call arrives in a device driver as a read IRP. The size and location of the read buffer are specified as parameters within the IRP structure. The IRP structure is fundamental to device drivers.

A driver has one main initialization entry point – a routine that is called DriverEntry; it has a standard function signature.

NTSTATUS 
 DriverEntry (
 PDRIVER_OBJECT pDriverObject,
 PUNICODE_STRING pRegistryPath 
)

The DriverObject parameter supplies the DriverEntry routine with a pointer to the driver's driver object, which is allocated by the I/O manager. The DriverEntry routine must fill in the driver object with entry points for the driver's standard routines.

The DriverObject pointer gives the driver access to DriverObject->HardwareDatabase, which points to a counted Unicode string that specifies a path to the Registry's \Registry\Machine\Hardware tree.

The Registry path string pointed to by RegistryPath is of the form \Registry\Machine\System\CurrentControlSet\Services\DriverName. A driver can use this path to store driver-specific information. The DriverEntry routine should save a copy of the Unicode string, not the pointer, since the I/O manager frees the RegistryPath buffer after DriverEntry returns.

The kernel calls the DriverEntry routine when a driver is loaded. Subsequently, it may call many other functions in the driver. The functions are generally called callback functions. When a driver is initialized, it registers all callback functions it supports to the kernel. Then, the kernel calls the callback function in the right circumstances. For example, if a driver supports read operations, Read callback functions should be registered to the kernel. Each callback has a standard function prototype, appropriate for the circumstances in which it is called.

Standard driver entry points:

  • DriverEntry: Initial driver entry point. Registers all callbacks to the kernel.
  • Unload: Unloads the driver.
  • StartIO: A callback to handle an IRP serially.
  • Interrupt Service Routine (ISR): Called to handle a hardware interrupt.
  • IRP Handlers: Called to process the IRPs that you wish to handle.

The following code snippet shows a callback registration to the kernel:

C++
// Announce other driver entry points
pDriverObject->DriverUnload = DriverUnload;
 
// This includes Dispatch routines for Create, Write & Read
pDriverObject->MajorFunction[IRP_MJ_CREATE]     =DispatchCreate;
pDriverObject->MajorFunction[IRP_MJ_CLOSE]      =DispatchClose;
pDriverObject->MajorFunction[IRP_MJ_WRITE]      =DispatchWrite;
pDriverObject->MajorFunction[IRP_MJ_READ]       =DispatchRead;

The callback function's prototype is as follows:

C++
NTSTATUS
DispatchXXX(
    IN PDEVICE_OBJECT  DeviceObject,
    IN PIRP  Irp
    );

The Operating System represents devices by device objects. Drivers create device objects by using the IoCreateDevice and IoCreateDeviceSecure routines. A device object is partially opaque. Drivers do not set members of the device object directly, unless otherwise documented.

An IRP is the basic I/O manager structure used to communicate with drivers and to allow drivers to communicate with each other. A packet consists of two different parts:

  • Header, or fixed part of the packet: This is used by the I/O manager to store information about the original request, such as the caller's device-independent parameters, the address of the device object upon which a file is open, and so on. It is also used by drivers to store information such as the final status of the request.
  • I/O stack locations: Following the header is a set of I/O stack locations, one per driver in the chain of layered drivers for which the request is bound. Each stack location contains the parameters, function codes, and context used by the corresponding driver to determine what it is supposed to be doing. For more information, see the IO_STACK_LOCATION structure.

While a higher-level driver might check the value of the Cancel boolean in an IRP, that driver cannot assume the IRP will be completed with STATUS_CANCELLED by a lower-level driver even if the value is TRUE.

All IRP handles or callback functions are called by the kernel only after creating a Device object. We have seen that all IRP handle prototypes require a device object that is created by the IoCreateDevice function.

The driver will create a Device object before the DriverEntry function exits. IoCreateDevice creates a device object and returns a pointer to the object. The caller is responsible for deleting the object when it is no longer needed, by calling IoDeleteDevice.

IoCreateDevice can only be used to create an unnamed device object, or a named device object for which a security descriptor is set by an INF file. Otherwise, drivers must use IoCreateDeviceSecure to create named device objects. The devices created by IoCreateDevice are not visible from user space and Symbolic links are created for Windows applications to access the devices. The IoCreateSymbolicLink routine sets up a symbolic link between a device's object name and a user-visible name for the device.

DriverEntry code explanation is over. The next part describes the callback functions and its implantation details.

Unload Callback Implementation

The Unload routine is required for WDM drivers and is optional for non-WDM drivers. A driver's Unload routine, if supplied, should be named XxxUnload, where Xxx is a driver-specific prefix. The driver's DriverEntry routine must store the Unload routine's address in DriverObject->DriverUnload. (If no routine is supplied, this pointer must be NULL.)

The Operating System unloads a driver when the driver is being replaced, when the device's driver services have been removed, or when driver initialization fails.

The I/O manager calls a driver's Unload routine when all of the following are true:

  • No references remain to any of the device objects the driver has created. In other words, no files associated with the underlying device can be open, nor can any IRPs be outstanding for any of the driver's device objects.
  • No other drivers remain attached to this driver.
  • The driver has called IoUnregisterPlugPlayNotification to unregister all PnP notifications for which it previously registered.

Note that the Unload routine is not called if a driver's DriverEntry routine returns a failure status. In this case, the I/O manager simply frees the memory space taken up by the driver.

Neither the PnP manager nor the I/O manager calls Unload routines at system shutdown time. A driver that must perform shutdown processing should register a DispatchShutdown routine.

Below is the code snippet for an Unload routine:

C++
VOID DriverUnload (PDRIVER_OBJECT   pDriverObject) 
{
    PDEVICE_OBJECT    pNextObj;
     
    // Loop through each device controlled by Driver
    pNextObj = pDriverObject->DeviceObject;
     
    while (pNextObj != NULL) {
        PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pNextObj->DeviceExtension;

        //...
        //  UNDO whatever is done in Driver Entry
        //

        // ... delete symbolic link name
        IoDeleteSymbolicLink(&pDevExt->symLinkName);
        pNextObj = pNextObj->NextDevice;

        // then delete the device using the Extension
        IoDeleteDevice( pDevExt->pDevice );
    }
    return;
}

Create Callback Implementation

A driver's DispatchCreate routine should be named XxxDispatchCreate, where Xxx is a driver-specific prefix. The driver's DriverEntry routine must store the DispatchCreate routine's address in DriverObject->MajorFunction [IRP_MJ_CREATE].

Input parameters for all Dispatch routines are supplied in the IRP structure pointed to by Irp. Additional parameters are supplied in the driver's associated I/O stack location, which is described by the IO_STACK_LOCATION structure and can be obtained by calling IoGetCurrentIrpStackLocation.

NTSTATUS DispatchCreate (PDEVICE_OBJECT   pDevObj, PIRP pIrp) 
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;    
 
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;     // no bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return STATUS_SUCCESS;
}

Close Callback Implementation

A driver's DispatchClose routine should be named XxxDispatchClose, where Xxx is a driver-specific prefix. The driver's DriverEntry routine must store the DispatchClose routine's address in DriverObject->MajorFunction [IRP_MJ_CLOSE].

NTSTATUS DispatchClose (IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
    ULONG i,thNum;
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
 
    // . . .
    // Deallocates memory, Deregister functions, ISR
    // IF allocated in either DriverEntry, DistpatchCreate, …etc.
    // . . .
 
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;     // no bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return STATUS_SUCCESS;
}

Read Callback Implementation

A driver's DispatchRead routine should be named XxxDispatchRead, where Xxx is a driver-specific prefix. The driver's DriverEntry routine must store the DispatchRead routine's address in DriverObject->MajorFunction [IRP_MJ_READ].

NTSTATUS DispatchRead (PDEVICE_OBJECT     pDevObj,PIRP pIrp) 
{     
    NTSTATUS status = STATUS_SUCCESS;
    PVOID userBuffer;
    ULONG xferSize,i,thNum ;
 
    // The stack location contains the user buffer info
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
 
    // Dig out the Device Extension from the Device object
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
 
    // Determine the length of the request
    xferSize = pIrpStack->Parameters.Read.Length;
    userBuffer = pIrp->AssociatedIrp.SystemBuffer;
      
    //
    // copy to user buffer from system buffer
    //
 
    // Now complete the IRP
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = xferSize;    // bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return status;
}

Write Callback Implementation

A driver's DispatchWrite routine should be named XxxDispatchWrite, where Xxx is a driver-specific prefix. The driver's DriverEntry routine must store the DispatchWrite routine's address in DriverObject->MajorFunction [IRP_MJ_WRITE].

C++
NTSTATUS DispatchWrite (IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp) 
{     
    NTSTATUS status = STATUS_SUCCESS;
    PVOID userBuffer;
    ULONG xferSize;
    ULONG i,thNum;
 
    // The stack location contains the user buffer info
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );

    // Dig out the Device Extension from the Device object
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
 
    // Determine the length of the request
    xferSize = pIrpStack->Parameters.Write.Length;
    // Obtain user buffer pointer
    userBuffer = pIrp->AssociatedIrp.SystemBuffer;
 
    // Allocate the new buffer and copy from user Buffer. 
 
    // Now complete the IRP
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = xferSize;    // bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return status;
}

User Application

The user application is a simple Win32 console application; it opens a file, writes a buffer, and reads the buffer from the driver and then closes the file. Unlike Linux, opening a file and a device in Windows is different.

Windows uses CreateFile, ReadFile, WriteFile, and CloseHandle calls to open or create device files, write to a device, read from a device, and finally to close the handle.

A snippet follows:

C++
int main() {
    HANDLE hDevice;
    BOOL status;
 
    hDevice = CreateFile("\\\\.\\LBK",
                    GENERIC_READ | GENERIC_WRITE,
                    0,          // share mode none
                    NULL, // no security
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL );           // no template
 
    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("Failed to obtain file handle to device: "
              "%s with Win32 error code: %d\n",
              "LBK1", GetLastError() );
        return 1;
    }
 
    status = WriteFile(hDevice, outBuffer, outCount, &bW, NULL);
    if (!status) {
        printf("Failed on call to WriteFile - error: %d\n",
              GetLastError() );
        return 2;
    }
 
    . . .
 
    status = ReadFile(hDevice, inBuffer, inCount, &bR, NULL);
    if (!status) {
        printf("Failed on call to ReadFile - error: %d\n",
              GetLastError() );
        return 4;
    }
 
    status = CloseHandle(hDevice);
    if (!status) {
        printf("Failed on call to CloseHandle - error: %d\n",
               GetLastError() );
        return 6;
    }
    printf("Succeeded in closing device...exiting normally\n");
    return 0;
}

Run the Application

The given sample code is built by using the DDK console. If DDK is installed, open the DDK build console, and go to project HOME directory. Build the driver as well as the application by using build –cEZ.

Once the driver is built, then it should be copied to the $WINDOWS$\system32\drivers folder. Make an entry in the Rregistry in the services path. One .reg file is attached in the sample. By double clicking the .reg file, it would update the Registry. It is better to reboot the system after updating the Windows Registry. It works without rebooting the system, but Windows documentation says when the Registry is updated, the system should be rebooted for the changes to take effect.

After the Registry is updated, start the service. There are many utilities that are available on the net for starting the service in Windows. The same can be done by a simple Windows command:

net start <service-name> # in our case service is driver name

The driver is now ready to communicate with a user application. Run the user application, Testor.exe. It will be in the Testor executable folder.

History

This is the first version. I will make sure that whenever I update the sample, I will upload it to The Code Project. This is a simple WDM sample. I will be posting a few more Windows samples in the future.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)