Click here to Skip to main content
15,036,145 members
Articles / Programming Languages / C
Posted 4 Jun 2013

Tagged as


36 bookmarked

Windows Device Drivers

Rate me:
Please Sign up or sign in to vote.
3.94/5 (7 votes)
4 Jun 2013CPOL19 min read
Windows Device Drivers

1. Introduction

The Windows Driver model provides a framework for device drivers that operate in two operating systems, Windows 98/Me and Windows 2000/XP. In this report I will discuss the aspect of device driver programming related to Windows XP. Every platform where XP runs, it supports two modes of execution, user mode and kernel mode. A user mode program call an API for any function like ReadFile and this API is implemented in a subsystem module like KERNEL32.DLL. This subsystem then again calls the function in a native API such as NtReadFile. There are many routines that serve purposes similar to NtReadFile and they operate in kernel mode in order to service a request to interact with devices. User-mode programs don’t have to implement these functions, they just create a data structure called an I/O request packet (IRP) and they pass to an entry point in device driver. In the above case of ReadFile call, NtReadFile would create an IRP with a major function code IRP_MJ_READ (a constant in device driver development kit header file). A device driver may need to access its hardware to perform IRP. After a driver has finished an I/O operation, it completes the IRP by calling a particular kernel mode service routine.

The figure bellows shows the different types of device drivers in windows XP.

A Virtual Device Driver (VDD) allows MS DOS applications to access hardware on Intel x86. Kernel mode drivers have many sub categories. A PnP drives is a plug and play driver and WDM is a PnP driver that understands power management protocols. File System Drivers implements the File System on hard drives and Legacy drivers are kernel mode drivers that directly control a device without the need of any other device driver. For most of the devices you have to write the WDM drivers as Microsoft doesn’t provide support for them. In the next section I will describe the structure of WDM device driver.

2. Structure of the WDM driver

A driver is a collection of functions that operating system calls to perform various operations that is related to the hardware. The figure below shows some of the operations

Some of the functions above DriverEntry, AddDevice and some of the Dispatch routines are generally present in every device driver. Drivers that perform memory access will have AdaptorControl routine. Driver for devices that generate hardware interrupts will have an interrupt service routine and a deferred procedure call routine. Like a ‘C’ or ‘C++’ executable file, device driver is also an executable file and has a file extension .SYS. A driver can also have some supporting library which contains functions required by the driver. It can also have debugging information and resource data. A driver doesn’t have a ‘main’ function like ‘C’ programs but it is a collection of subroutines that a system can call. The steps an operating system generally follows while calling functions in device driver are as follows

  • When a device is plugged into system, the system loads the driver in virtual memory and calls the DriverEntry routine. This routine then performs some operations and returns.
  • The PnP manager then calls the AddDevice function and returns.
  • PnP manager then sends a few IRPs and dispatch function processes each IRP and returns.
  • An application open a handle to the device and the system sends another IRP then dispatch routine performs some work and returns.
  • Application then tries to read data and the system sends you and IRP and dispatch routine puts that IRP in queue and returns.
  • An I/O operation finishes by signaling the hardware interrupt to which the driver is connected. Your interrupt routine does a little bit of work, schedule a DPC and returns.
  • Your DPC routine runs and it removes the IRP scheduled in queue in above step and programs the hardware to read the data and then returns.
  • System makes more calls to the driver and finally when the user unplugs the device, the PnP manager sends the IRPs, which you process and return. The OS then calls the DriverUnload routine and that do some work and returns.

A driver doesn’t create a new thread unlike the ‘C’ programs but executes in the context of whatever thread happens to active at that time. System doesn’t always execute driver code in arbitrary thread, a driver can create its own system threads by calling PsCreateSystemThread. Knowing the thread context is sometime necessary like a driver should not block arbitrary thread because it would be unfair to block any thread and the other reason is when a driver creates an IRP to send to some other driver, you need to create a IRP in arbitrary thread but you might create a different kind of IRP in a non-arbitrary thread. The I/O manager ties the synchronous kind of IRP to the thread within which you create the IRP. It will cancel the IRP automatically if the thread terminated so it is incorrect for the system to cancel the IRP just because that thread is terminating.

The System detects the Plug and Play device by its electronic signature that the system can detect. For Plug and Pay devices, a system bus driver detects the existence of hardware and reads the signature to determine what kind of hardware it is. A legacy driver doesn’t have any signature so the used must initiate the detection process by starting “add new hardware” wizard.

In the Windows Driver model each hardware device has at least two drivers. One of these drivers is called function driver which is responsible for almost every work like he hardware work, I/O operations, handling of interrupts and control over the device. The other driver is bus driver that is responsible for managing the connection between hardware and computer for example the bus driver for Peripheral Component Interconnect is the software component that actually detects the card plugged into the PCI slot. Layering of driver and device objects are shown in figure below

If an IRP is generated then the Upper filter will first get it before the function driver. The Upper filter driver and Lower filter drivers may or may not present in device drivers and they are generally provided to support the function and bus drivers. Some extra functionality can be included in these drivers which we want to perform before running the actual drivers. The left column in the above figure shows the upwardly linked stack of kernel DEVICE_OBJECT structures.

  • PDO: It stands for physical device object and the bus driver use this object to represent the connection between device and bus.
  • FDO: It stands for function device object and the function driver uses it to manage functionality of devices.
  • FiDO: It stands for filter device object and used by filter driver to store the information it needs to keep about the hardware and filtering activities.

When a bus driver detects the insertion or removal of hardware, it calls IoInvalidateDeviceRelations to notify the PnP Manager that the bus’s population of child devices has changed. To obtain an updated list of the PDOs for the child devices, the PnP Manager sends an IRP to the bus driver.

In response to the PnP manager’s bus response query, the bus driver returns a list of PDOs. The PnP manager then determines which PDOs represent devices that are not initialized. PnP manager then again send another IRP to bus driver and in return bus driver send the device ID. PnP manager then used this ID to locate a hardware key in the system registry.

Installation instructions for all types of hardware exist in files with the extension .INF. Each INF contains statements that relate particular device identifier strings to install sections within that INF file. When a new device comes then the system tries to find INF file containing the install sections corresponding to the device identifier string. It is your responsibility to provide this file that’s why the above figure contains You.

Data Structures

The I/O manager uses a driver object data structures to represent each device driver. The DDK (device driver development SDK provided by Microsoft) header declares the entire structure and all other kernel mode data structures like the one shown below.

typedef struct _DRIVER_OBJECT {
  CSHORT Type;  CSHORT Size; 

That is, the header declares a structure with the type name DRIVER_OBJECT. It also declares a pointer type (PDRIVER_OBJECT) and assigns a structure tag (_DRIVER_OBJECT).

The figure below shows the Driver_Object data structure.

  • The DeviceObject contains a list of device object data structures for each of the devices managed by the driver. The I/O manager maintains this field and the DriverUnload would use this field to traverse to the list of devices in order to delete them.
  • The DriverExtension points to a data structure which contains many fields but only AddDevice field is accessible. This field is pointer to a function within the driver creates device objects.
  • HardwareDatabase contains a string that names a hardware database registry key for the device some thing like \Registry\Machine\Hardware\Description\System.
  • FastIoDispatch points to a table of functions pointers that file system and network drivers export.
  • DriverStartIo points to a function in your driver that processes I/O requests that the I/O Manager has serialized.
  • DriverUnload points to a cleanup function in your driver.
  • MajorFunction is a table of pointers to functions in your driver that handle various I/O request.

Like the DriverObject for device drivers, we also have a device object for devices. The figure below shows the Device_Object data structure

  • DriverObject points to the object describing the driver associated with the device.
  • NextDevice points to the next device object that is associated with the same driver.
  • CurrentIrp is used by the Microsoft IRP queuing routines StartPacket and StartNextPacket to record the IRP most recently sent to StartIo routine.

Flags contain a collection of flag bits like DO_BUFFERED_IO for reads and write to use buffered method or DO_DIRECT_IO for reads and write to use Direct method

Characteristics is another collection of flag bits describing various optional characteristics of the device like media can be read not written, media is floppy drive, media is virtual volume, device is accessible through network connection and some more

DriverEntry Routine

In the previous sections we discussed that the PnP manager loads the driver and calls the AddDevice function. A driver can be used for more than one device so there is need for some global initialization which is done only the first time driver is loaded and is the responsibility of DriverEntry routine.

extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,

The first argument to the function is the object of the driver. WDM driver’s DriverEntry function will finish initializing this object and return. Non-WDM drivers have a great deal of extra work to do—they must also detect the hardware for which they’re responsible, create device objects to represent the hardware, and do all the configuration and initialization required to make the hardware fully functional. PnP manager do all this stuff for WDM drivers because they are plug and play. The second argument is the service key in the registry and this value is not persistent so this should be stored in some variable if planned to use later.

The main job of this function is to fill the driver object with different function pointer like

  • DriverUnload: set this to point where cleanup function is written for driver
  • DriverExtension->AddDevice: set this to point to AddDevice function. PnP manager will call this function for each device the driver is responsible.
  • DriverStartIO: If driver uses the standard method of queuing I/O requests, set this to point to your StartIo routine.
  • MajorFunction: Driver handles some IRPs so this contains a list of function with each one handling a IRP.

A sample code for the DriverEntry function is given below for better understanding:

extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
  DriverObject->DriverUnload = DriverUnload;
  DriverObject->DriverExtension->AddDevice = AddDevice;

  DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
  DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
  DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = 

  servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool,
    RegistryPath->Length + sizeof(WCHAR));
  if (!servkey.Buffer)
  servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR);
  RtlCopyUnicodeString(&servkey, RegistryPath);
  servkey.Buffer[RegistryPath->Length/sizeof(WCHAR)] = 0;

AddDevice Routine

PnP manager call this function for each of the devices associated with the driver. The function has the following representation:


The first argument points to the same DriverObject that was initialized in the DriverEntry routine. The pdo argument is the address of the physical device object at the bottom of the device stack. Some of the responsibilities of this function are as follows

  • It calls the IoCreateDevice to create a device object 
  • Register interfaces for the devices so that application knows about the existence of device or give device object a name and create a symbolic link
  • Initialize device extension and the flags of the device object
  • Call IoAttachDeviceToDeviceStack to put device in the device stack

Windows XP uses a Centralized Object Manager to manage its internal data structures including the device and driver objects. The purpose of giving device object name is so that applications can open handle to device and send IRPs. 

3. Driver Development Issues

Some of the device driver development issues are as following

  • Driver Design Strategies
  • Coding Convention and Techniques
  • Driver Memory Allocation
  • Unicode Strings
  • Interrupt Synchronization
  • Synchronizing Multiple CPUs

Driver Design Strategies

Traditionally writing a device driver doesn’t require a design strategy but as the length of device drivers are becoming long, the need for design strategy arises.

Some of the design techniques are listed below

  • Data flow diagrams can help break a driver into functional units. These diagrams make it easier to see how the functional units in a driver relate to each other.
  • State-machine models are another good way to describe the flow of control in a driver
  • Another useful tool is the list of external events and the driver actions that these events should trigger. The list should include both hardware events from the device and software I/O requests from users.

Using these techniques helps in decomposing a device driver into smaller functional units. Once the initial analysis and design is done some of the steps given below can help in reducing the debugging time

  • Decide which kind of kernel mode objects a driver need
  • Code the DriverEntry and Unload routines first. Do not add plug and play support because this makes it easy to test from console.
  • Add dispatch routines that process IRP_MJ_Create and IRP_MJ_Close operations. These routines doesn’t require device access so the driver can easily be tested with a simple application
  • Add code that will find the hardware and allocates memory to objects and also adds the code to deallocate and unload the driver
  • Add dispatch routines that process any other IRP_MJ_XXX function codes.
  • Finally implement the interrupt service routine and Start I/O logic and DPC routine

Coding Convention and Techniques

The assembly language should not be used in driver. It makes the code hard to read, non-portable and difficult to maintain. For platform-specific code, #ifdef/#endif directives should be used. Driver should not be linked to standard C library as this is waste of space and also some routines in library are not thread and context safe. Besides including NTDDK.h or WDM.h, a driver should use private header files to hide various hardware and platform dependencies. Some compilers support the option of declaring certain functions as discardable. Functions in this category will disappear from memory after a driver has finished loading, making the driver smaller. If the development environment offers this feature, it should be used. Discardable functions can be DriverEntry and any functions called by DriverEntry. The code shown below can be used to discard the functions from memory

#pragma alloc_text( init, DriverEntry )
#pragma alloc_text( init, FuncCalledByDriverEntry )
#pragma alloc_text( init, OtherFuncCalledByDriverEntry )

Non-paged system memory is important for the system and should be saved. A driver can choose the routine it want in the Paged memory so that the burden can be reduced over non-paged memory.

Any function that runs only at PASSIVE_LEVEL IRQL can be paged. This includes Reinitialize routines, Unload and Shutdown routines, Dispatch routines, thread functions, and any helper functions running exclusively at PASSIVE_LEVEL IRQL. Again the alloc_text is used for this purpose. An example to do this is shown below:

#pragma alloc_text( page, Unload )
#pragma alloc_text( page, Shutdown )
#pragma alloc_text( page, DispatchRead )
#pragma alloc_text( page, DispatchHelper )

Driver Memory Allocation

Memory is one of the important aspects of programming and the drivers don’t have anything like malloc and free or new and delete. So care must be taken so that right type of memory is allocated. They should also write cleanup modules as there is no automatic mechanism for kernel-mode code. Three options for allocating storage are available and the criterion is based on size and duration of the request. The three options are Kernel Stack, Paged Pool and Non-Paged Pool. The first one provides limited amount of non-paged storage. As the code in driver is reentrant, global variables are avoided. While working with kernel stack we need to careful to avoid overflow of data. Some of the guidelines are

  • Don't design a driver in such a way that internal routines are deeply nested.
  • Avoid recursion or limit the depth if unavoidable.
  • Do not use the kernel stack to build large data structures. Use one of the pool areas instead.

Unicode Strings

Character strings are internally stored as Unicode in Windows operating systems. This scheme uses 16 bits to represent each character and increases the portability. Windows XP works with a Unicode Structure to make it easier to pass the Unicode strings. DDK also defines an ANSI_STRING structure which is almost similar to UNICODE_STRING structure. Working with Unicode can be frustrating because the length in bytes of a Unicode string is twice the content length. C or C++ programmers generally consider a character to be of one byte. When working with Unicode, we should keep in mind some of the points given below

  • The number of characters in Unicode strings is not same as the number of bytes so care should be taken while doing any arithmetic which calculated the length of the Unicode string.
  • Nothing should be assumed about the collating sequence of characters or the relation between upper and lowercase characters
  • Don’t assume that a table with 256 entries is able to store the entire character set.

Interrupt Synchronization

Reentrant code at multiple IRQL level requires proper synchronization. If code executing at two different IRQLs attempts to access the same data structure simultaneously, the structure can become corrupted. The figure below shows the problems

Suppose a code executing a low IRQL modifies foo.x to 1 and a high IRQL code interrupts the low IRQL and sets foo.x to 10 and foo.y to 20, when the control returns back to low IRQL it is unaware of the changes done by high IRQL and changes foo.y to 2 so the data finally is in inconsistent state.

Synchronizing Multiple CPUs

Modifying the IRQL of one CPU has no effect on other CPUs and multiprocessor systems. IRQLs provide only local CPU protection to share data. To prevent corruption of data structures in a multiprocessor environment, Windows XP uses synchronization objects called spin locks.

Spin lock is a mutual-exclusion object that is related to some data structures. When a piece of kernel-mode code wants modify these data structures, it must first request ownership of the associated spin lock. Since only one CPU at a time can own the spin lock, the data structure is safe from any damage. Any CPU requesting an already-owned spin lock will busy-wait until the spin lock becomes available. The figures below explains the process

4. Driver Dispatch Routines

Before a driver can process any request, it must give details of the operations it supports. In this section I will talk about the I/O managers dispatching operation and will explain about how to enable a device driver to receive I/O function codes. In Windows XP I/O manager keeps track of all the I/O request initiated in the system by creating a IRP work-order. It keeps a function code for the I/O request in the MajorField of the IRP’s I/O stack location. This MajorField is used by the I/O manager for mapping with the Drivers MajorFunction table. This table contains a dispatch routine for each kind of requests. A pictorial representation of this is shown below

To enable I/O function codes, a driver must define the dispatch function that will handle the I/O function code. This definition is done in the DriverEntry procedure that stores the address of the dispatch function at the appropriate location in MajorFunction array. The code to perform this operation is shown below:

IN PUNICODE_STRING registryPath) {
pDO->MajorFunction[ IRP_MJ_CREATE ] = Create;
pDO->MajorFunction[ IRP_MJ_CLOSE ] = Close;
pDO->MajorFunction[ IRP_MJ_READ ]= Read;

IRP_MJ_XXX is a symbol which represents the I/O function code and is declared in the NTDDK.h. The function codes which a driver doesn’t support should be let untouched because the I/O manager fills all the entries in MajorFunction by a default value before calling the DriverEntry. Some of the functions codes must be supported by the driver like IRP_MJ_CREATE and IRP_MJ_ClOSE since these codes are generated in response to Win32 CreateFile and CloseHandle call. When the driver is in layered form, then higher level driver must support the functions codes lower level driver supports as the call always goes from higher to lower level driver.

The dispatch routines have the same signature and these routines runs at PASSIVE_LEVEL_IRQL which means that they can access paged system resources. A sample dispatch routine which rejects a IRP request looks like this:

IN PIRP irp ) {
//This means the request is not supported
//so reject it
irp ->IoStatus.Status = STATUS_NOT_SUPPORTED;
// this means no bytes were transfered
irp ->IoStatus.Information = 0;
// set the IRP as complete and no priority change
IoCompleteRequest(irp, IO_NO_INCREMENT);

A dispatch routine completing a request looks like this

IN PIRP irp ) {
irp ->IoStatus.Status = STATUS_SUCCESS;
// this means no bytes were transfered
irp ->IoStatus.Information = 0;
// mark the irp complete 
IoCompleteRequest(irp, IO_NO_INCREMENT );

5. Driver Installation

The installation of the driver is controlled by an .INF extension file. The installation of the driver changes the system registry entries with the new driver information. An INF file is a simple text file divided into sections, with each section designated by an identifier within closed braces ([ ]). Some section names are required, while others are driver-specific. Entries under each section control some installation action, or they link to other sections. The order of the sections in this file is not important and a section continues until another section started or end of file is encountered. The general format of section entries is entry = value [, value...]

where entry is a directive, keyword, or filename, and value is the attribute that is to be applied to entry. After the INF file is created, it must be processed to make the things working. Manual or automatic installation can be done with the INF file. By opening the INF in a windows explorer and selecting an install option after right clicking the file, starts the installation process or in a Plug and Play environment, installation can be started by insertion or removal of the device. Sometimes Add/Remove hardware wizard is needed along with automatic installation. A segment of the INF files looks like the one given below


6. References

  1. The Windows 2000 Device Driver Book, A Guide for Programmers, Second Edition by Art Baker and Jerry Lozano
  2. Programming the Microsoft Windows Driver Model by Walter Oney
  3. Linux Device Drivers, 2nd Edition By Alessandro Rubini and Jonathan Corbet
  4. Writing Windows Wdm Device Drivers: Covers Nt 4, Win 98, and Win 2000 by Chris Cant



#include <wdm.h>
#include "Header.h"


#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, Unload)

UNICODE_STRING DriverName, DosDeviceName;
unsigned int Index = 0;

DbgPrint("DriverEntry function has been called \n");
RtlInitUnicodeString(&DriverName, L"\\Device\\New");
RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\New"); 

NtStatus = IoCreateDevice(pDObject, 0, &DriverName, 

if(NtStatus == STATUS_SUCCESS)

for(Index = 0; Index < IRP_MJ_MAXIMUM_FUNCTION; Index++)
pDObject->MajorFunction[Index] = UnSupportedFunction;

pDObject ->MajorFunction[IRP_MJ_CLOSE] = Close;
pDObject ->MajorFunction[IRP_MJ_CREATE] = Create;
pDObject ->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoControl;
pDObject ->MajorFunction[IRP_MJ_READ] = USE_READ_FUNCTION;
pDObject ->MajorFunction[IRP_MJ_WRITE] = USE_WRITE_FUNCTION;

pDObject ->DriverUnload = Unload; 
pDeviceObject->Flags |= IO_TYPE;
pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING); 

IoCreateSymbolicLink(&DosDeviceName, &DriverName);

return NtStatus;


DbgPrint("Unload function has been called \n"); 
RtlInitUnicodeString(&DeviceName, L"\\DosDevices\\New");





#ifndef IO_TYPE
#define IO_TYPE 0
#define USE_WRITE_FUNCTION WriteNone
#define USE_READ_FUNCTION ReadNone


#include <wdm.h>
#include "Header.h"
#pragma alloc_text(PAGE, Create) 
#pragma alloc_text(PAGE, Close) 
#pragma alloc_text(PAGE, IoControl) 
#pragma alloc_text(PAGE, ReadNone)
#pragma alloc_text(PAGE, WriteNone)
#pragma alloc_text(PAGE, NotSupportedFunction)

  DbgPrint("Create function has been called \n");
  return NtStatus;

  DbgPrint("Close function has been called \n");
  return NtStatus;

  DbgPrint("IoControl function has been called \n");
  return NtStatus;

  DbgPrint("ReadNone function has been called \n");
  return NtStatus;

  DbgPrint("WriteNone function has been called \n");
  return NtStatus;

  DbgPrint("NotSupportedFunction function has been called \n");
  return NtStatus;


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Web Developer
United States United States
Software Engineer
Houston TX

Comments and Discussions

Questionyou need to create a IRP Pin
ellegonzalez24-Jan-17 6:22
Memberellegonzalez24-Jan-17 6:22 
QuestionWrong. Pin
Munchies_Matt9-Dec-16 1:36
MemberMunchies_Matt9-Dec-16 1:36 
GeneralMy vote of 3 Pin
imagiro4-Jun-13 22:51
Memberimagiro4-Jun-13 22:51 
GeneralMissing images Pin
Smitha Nishant4-Jun-13 6:25
protectorSmitha Nishant4-Jun-13 6:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.