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:
pDriverObject->DriverUnload = DriverUnload;
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:
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:
VOID DriverUnload (PDRIVER_OBJECT pDriverObject)
{
PDEVICE_OBJECT pNextObj;
pNextObj = pDriverObject->DeviceObject;
while (pNextObj != NULL) {
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pNextObj->DeviceExtension;
IoDeleteSymbolicLink(&pDevExt->symLinkName);
pNextObj = pNextObj->NextDevice;
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;
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;
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
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 ;
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
xferSize = pIrpStack->Parameters.Read.Length;
userBuffer = pIrp->AssociatedIrp.SystemBuffer;
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = xferSize;
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]
.
NTSTATUS DispatchWrite (IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
NTSTATUS status = STATUS_SUCCESS;
PVOID userBuffer;
ULONG xferSize;
ULONG i,thNum;
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
xferSize = pIrpStack->Parameters.Write.Length;
userBuffer = pIrp->AssociatedIrp.SystemBuffer;
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = xferSize; 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:
int main() {
HANDLE hDevice;
BOOL status;
hDevice = CreateFile("\\\\.\\LBK",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL );
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.