The NT Insider

Converting Windows NT V4 Drivers to WDM/Win2K
(By: The NT Insider, Vol 5, Issue 2, Mar-Apr 1998 | Published: 15-Apr-98| Modified: 20-Aug-02)

As time goes by, the picture of what device drivers will be like in NT V5 has become clearer. While it will be "a while yet" until we actually see the final release of NT V5, we at least have a Beta release of the NT V5 DDK to play with. In this article, we?ll take a look at what?s going to be necessary to convert NT V4 device drivers to the WDM/NT V5 standard.

Don?t Do Nothin?


The first thing that?s important to understand is that existing Windows NT V4 device drivers (what we typically refer to as "standard kernel mode drivers") will continue to run unchanged on Windows NT V5. So, while these "legacy" drivers won?t support either power management or the new plug and play features, they will continue to work the way they always did.

While it might be heretical to say it, not converting your NT V4 device driver (at least not in the immediate future) may actually be the best strategy for many driver writers. This is most likely to be true if your hardware runs in an environment where support of power management or plug and play is unimportant. Since NT V5 drivers are not directly compatible with NT V4, converting your driver to run under NTV5 while still having an NT V4-based product means that you have two separate drivers to support. This is never a pleasant thought.

Does NT V5 = WDM?


However, let?s assume that you?ve got customers who want to Beta test your hardware under NT V5, that adding power management or plug and play is really valuable to you, or that you just can?t wait to play with NT V5 and see what happens. That?s cool. Let me tell you what has to change in your driver.

There?s good news and there?s bad news about WDM/NT V5 drivers. The really good news is that all those Windows NT systems internals and device driver concepts you?ve spent so much time learning are still valid. NT (and WDM) still implements a layered-driver architecture. In fact, there?s more layering than ever. There?s also the (sort of) good news that fully WDM compatible drivers will (or at least are supposed to) work on both NT V5 and Windows 9x.

However, don?t make the assumption that WDM drivers and Windows NT V5 drivers are exactly the same thing. They?re not. The WDM standard is a subset of the Windows NT V5 kernel mode driver standard. A driver can be written to be fully compliant with NT V5 standards, support plug and play and power management, and still not support WDM ? and therefore not be directly transportable to Windows 9x. WDM drivers compile with the file WDM.H, which includes a subset of definitions and features available in the wider NT-driver world. NT V5 standard kernel mode drivers, on the other hand, still include NTDDK.H and have access to all those features you?ve come to love.

What?s the difference? WDM drivers utilize a port and mini-driver concept similar to that used by SCSI Miniport drivers. WDM drivers are typically smaller, and almost exclusively contain support for device-specific features. These drivers are restricted to calling a sub-set of the standard NT APIs. Much, like connecting to interrupts, is handled for these drivers via their class drivers. While many of the familiar interfaces are indeed available to true WDM drivers, many old favorites such as HalGetAdapter(...), IoConnectInterrupt(...), and IoAssignResources(...) are replaced by the ability to do things "the WDM way". Other, less frequently used features, such as memory zones and ERESOURCES are also not supported by WDM.

On the other hand, NT V5 drivers include a set of new IRP_MJ functions, new driver entry points, and new processing methods. However, most of the old functions you?ve come to know and love are still present in the NT V5 kernel mode driver standard.

Remember that earlier I said that there was both good news and bad news? Well, the bad news is that since the NT V5 standard is a superset of WDM, not every NT V5 kernel mode driver will easily become a WDM driver. And, there are of course some things that you will need to update to allow your older driver to take advantage of the neat new NT V5 features.

Biting It Off


So, let?s say you want to upgrade your existing NT V4 standard kernel mode device driver to a full function NT V5 standard kernel mode driver ? but not necessarily a WDM driver. This conversion process is actually relatively straight forward.


What?s Where


In NT V4, DriverEntry(?) was the function in which we did everything necessary to initialize our driver and our devices. In DriverEntry(?) we export our entry points via the Driver Object. We also typically identify our device and create a Device Object for each device found. This ordinarily means scanning the bus to find our device(s), and calling IoCreateDevice(?). Next, in an NT V4 driver, we reserve the hardware resources to be used by our device. For PCI devices this typically means calling HalAssignSlotResources(?) and IoAssignResources(?) to get a CM_RESOURCE_LIST of the hardware resources that each of our devices will use. These resources include ports, shared memory areas, IRQs, DMA Channels, and whatever other hardware resources our device(s) require. We also connect to interrupts from our device within DriverEntry(?), and in general do whatever is necessary to become ready to process requests on our device.

In NT V5, the functions previously performed in DriverEntry(?) are separated out into three parts:

    1. Things to do with initializing the driver itself ? These things, like exporting our entry points, are still performed in DriverEntry(?).
    2. Things to do with discovering the devices our driver needs to support ? This step, including creating a Device Object, is now performed in the driver?s AddDevice(?) entry point.
    3. Things to do with device resources and device initialization ? Things to do with the device hardware itself actually wait until we?re called with an IRP_MJ_PNP IRP with an IRP_MN_START_DEVICE minor function. In this function, we do stuff like connect to interrupts.

Driver Entry in NT V5


Just like in NT V4, our DriverEntry(?) entry point is called in NT V5 at IRQL PASSIVE_LEVEL in the context of the system process when our driver is first loaded. DriverEntry(?) is called once per driver (that is to say, not once for each device owned by the driver).

DriverEntry(?) in NT V5 is typically restricted to doing things related to initializing the driver as a whole. This includes exporting our entry points via the Driver Object. Two new important entry points that NT V5 drivers will need to export are (1) an AddDevice(...) entry point, a pointer to which is placed in DriverObject->DriverExtension->AddDevice, and (2) the dispatch entry point for processing IRP_MJ_PNP IRPs (a pointer to which must be placed in DriverObject->MajorFunction[IRP_MJ_PNP]).

While not normally required, before exiting DriverEntry(...) you?ll have to save the Registry Path information if you?ll need it later. You should really only need this if you call one of those functions that requires it as a parameter (such as IoRegisterDriverReinitialization(...), HalAssignSlotResources(...), or IoAssignResources(...) ) or you need to do some Registry lookups in your AddDevice(...) routine (see below). Of course, good NT V5 drivers won?t typically call any of these routines (with the possible exception of IoRegisterDriverReinitialization(...)). If you do need to save the Registry Path, be sure to actually save the Registry Path data itself, not just a pointer to the registry path. The I/O Manager apparently deallocates the Registry Path immediately on return from its call to DriverEntry(...).

While it doesn?t say so in the preliminary NT V5 documentation, it is possible to still create Device Objects during DriverEntry(...). However, this is no longer typically done. In fact, the only Device Object you might want to create in DriverEntry(...) in an NT V5 driver would be an object for an over-all driver control type device. Perhaps this would be some sort of operation, administration, or management device. If you do create any Device Objects in DriverEntry(...), the DO_DEVICE_INITIALIZING bit is still cleared as before NT V5.

That?s really all there is to the "all new and improved" version of DriverEntry(...). What has traditionally been the longest function in many NT standard kernel mode drivers is now rather short indeed! Processing continues when the driver?s AddDevice(...) entry point is called.

The AddDevice(...) Routine


At some point after you?ve returned from DriverEntry(...), any time a device that is your driver?s responsibility is added to the system, the I/O Manager (or is it the Plug and Play Manager? oh, what?s the difference anyway!) calls your driver?s AddDevice(...) entry point. The prototype for this entry point is as follows:


NTSTATUS XxxxAddDevice (IN struct _DRIVER_OBJECT *DriverObject,

IN struct _DEVICE_OBJECT *PhysicalDeviceObject);

The DriverObject parameter passed into this routine is the same pointer passed into your DriverEntry(...) entry point. The PhysicalDeviceObject parameter is a pointer to the physical Device Object (PDO) that represents your device. This Device Object was created by the bus driver when it scanned the bus to see what devices were physically connected to the bus. The PDO is used as the point of communication between the bus driver, Plug and Play Manager, and your driver to inform you of PnP events (such as somebody disconnecting your device). See Figure 1, Functional and Physical Device Objects.

Figure 1 -- Functional and Physical Device Objects

When you?re called at your AddDevice(?) entry point here are two major activities for your driver to undertake:

    1. Create one or more Device Objects (and optionally Device Extensions) to represent your device. This is done in the traditional way by calling IoCallDriver(...). Don?t forget that because you?re calling IoCreateDevice(...) outside of your DriverEntry(...) entry point, you need to manually clear the DO_DEVICE_INITIALIZING bit in DeviceObject->Flags. This is an important detail, since unless this bit is cleared other Device Objects cannot be attached to yours. If you forget to clear this bit, in the checked build the I/O Manager will remind you with a little message. If required, also create a symbolic link to point to your Device Object using IoCreateSymbolicLink(...), just like in NT V4.
    2. Attach the newly created Device Object (or, indeed, Device Objects) to the physical device the bus driver has created to describe your device. This attach is done by calling IoAttachDeviceToDeviceStack(...), passing in the pointer to your newly created Device Object and the Physical Device Object passed into your AddDevice(...) entry point. IoAttachDeviceToDeviceStack(...) returns a pointer to the actual Device Object to which you attached. Be sure to save this away? you?re going to need it later.

Before leaving your AddDevice(...) entry point, you should perform any per-device initialization that can be performed without touching your hardware. This might include checking the Registry (if you saved the path away earlier!) for device-specific information or anything else you can dream up. But, to emphasize the point I?ll say it again, no references to your hardware are permitted in this function. You still haven?t been given your hardware resources. Your driver has only been informed that a device owned by your driver has been found. That comes in the next step.

Also, note that since you now have a Device Object it is entirely possible for users to issue I/O requests to that Device Object. Since you?re not allowed to touch your hardware yet, it would be a serious mistake to just go ahead and try to process any requests that you receive. Proper NT V5 etiquette requires that you keep track of the fact that this device has been created but not yet started (i.e. you?ve received an AddDevice(?) call for this device, but not an IRP_MN_START_DEVICE request, more about which later) and queue any IRPs that you receive for later processing on your device. The preliminary NT V5 documentation suggests keeping a flag in the Device Extension for this purpose? This sure seems like a good idea to me.

Leave AddDevice(...) with STATUS_SUCCESS if you were successful in your work in this routine. Returning an error status results in the load sequence for your driver being aborted.

Dispatch IRP_MJ_PNP


Here?s where things get really "interesting". Now, don?t shoot me, OK? I?m only the messenger.

When one of the previously added devices is to be started, the Plug and Play Manager will call your driver with an IRP containing an IRP_MJ_PNP major function code and an IRP_MN_START_DEVICE minor function code. IRP_MJ_PNP is used to identify IRPs that are queued to your driver as a result of plug and play events. There are seven minor function codes that uniquely identify the type of plug and play request to the driver. In WDM/NT V5, device drivers sit atop a driver stack that may include an underlying bus driver. This leads to two issues in a driver handling PNP requests:

    1. All IRP_MJ_PNP IRPs must be passed by your device driver to the underlying bus driver. This is vital for correct system operation.
    2. Some PNP IRPs must be processed (successfully) by the underlying bus driver before they can be processed by your device driver. On the other hand, some PNP IRPs need to be processed (successfully) by your driver before being passed on to the underlying bus driver.

It is your driver?s task to determine who processes each particular PNP IRP first (you or the underlying bus driver, depending on the IRP minor function code), and then (typically) to pass the IRP to the underlying driver in the normal way by calling IoCallDriver(...). The Device Object used as the target for the IoCallDriverCall(...) is the PDEVICE_OBJECT returned when the driver called IoAttachDeviceToDeviceStack(...) in its AddDevice(...) entry point. Fortunately, it?s pretty easy to figure out from the documentation who is supposed to handle which IRP_MN functions when. We?ve summarized this in the following table:


IRP_MN_ Function Code

Who Processes It First?


Bus driver


Device driver


Device driver


Bus driver


Device driver


Device driver


Bus driver


Bus driver

Table 1

When we say the device driver processes a request "first", we mean that on receipt the device driver examines the request. If the request can be accommodated, the device driver does what is necessary to carry out the request. When the device driver has completed processing the request successfully, it sends the request to the underlying bus driver. If the request cannot be accommodated, the device driver completes the request in the ordinary way with an appropriate error status. In this case, the IRP does not need to be passed to the underlying bus driver.

Passing an IRP on to another driver given a pointer to the target driver?s Device Object is done the same way in NT V5 as it was done in NT V4. The only difference is that in NT V5 we now have a handy macro to use to make things a bit easier. To pass a request to an underlying driver, you simply copy the current I/O Stack Location to the next I/O Stack Location, register a completion routine if you want one, and pass the IRP to the next driver using IoCallDriver(...). For the case where the device driver processes the IRP_MJ_PNP IRP first, a completion routine is not normally required, since if the PnP operation of which you?ve approved is not acceptable to the underlying bus driver, you?ll later get a PNP IRP telling you to cancel the operation. Thus, the code for this case would be as simple as the following:

// Invoke the handy stack copy macro, new to NT V5.
// Send the request to the bus driver and return
return(IoCallDriver(DeviceExtension->PdoPointer, Irp));

Hardly anything to worry about, right?

Unfortunately, when the bus driver processes the request first, things are a bit more tricky. And, to indicate that your device can be started, you receive an IRP_MN_START_DEVICE IRP that needs to be processed by the bus driver first. On receiving such a request, the device driver passes it to the bus driver without itself doing any processing of the request.

Again, just like in NT V4, the way a driver passes a request to an underlying driver and is later notified about completion of the request is by setting a completion routine in the IRP prior to passing the IRP to the underlying driver. The I/O Manager will call the completion routine when the underlying driver(s) have completed the request. Only when the completion routine has been called may the IRP actually be processed by the device driver. Unfortunately, recall that completion routines may be called at IRQL >= DISPATCH_LEVEL. This makes completion processing more complex than you might like.

While there are many ways to actually code-up the solution to this problem, we agree with the preliminary DDK that the best solution is to wait on an event in the device driver?s Dispatch Routine. When the completion routine is called, it signals the event, thus awakening the Dispatch Routine code, where the IRP is processed to completion. The completion routine re-claims "ownership" of the IRP by returning STATUS_MORE_PROCESSING_REQUIRED to the I/O Manager.

 // the new macro supplied with NT V5 strictly for this purpose


IoCopyCurrentIrpStackLocationToNext (Irp);


// Set a completion routine for this IRP. Have it called regardless

// of the IRP's completion status. The context passed into the

// completion routine is a pointer to the event to signal.


IoSetCompletionRoutine (Irp,







// Initialize an event which will be signaled from the

// completion routine.





status = IoCallDriver(devExt->NextDriverObject, Irp);


// Wait on the event to be signaled by the completion routine.

// The completion routine will "re-claim" the IRP so we may

// continue to process it below.








// After the completion routine wakes us, get the ultimate

// status of the operation from the IRP.


status = Irp->IoStatus.Status;

if (NT_SUCCESS (status)) {


// Since the bus driver was happy, we can FINALLY try to

// process the IRP.


status = OsrProcessPnPIrp(Irp);



// Since the completion routine ALWAYS reclaims the IRP by

// returning STATUS_MORE_PROCESSING_REQUIRED, we need to

// actually complete the IRP here.


Irp->IoStatus.Status = status;

Irp->IoStatus.Information = 0;

IoCompleteRequest (Irp, IO_NO_INCREMENT);

Not hard, right? As previously described, this code not only passes the IRP to the underlying driver, it also creates an event and waits for that event to be signaled. The event is set to signaled from the driver?s completion routine, shown below:


OsrPnpCompRoutine(IN PDEVICE_OBJECT DeviceObject,


IN PVOID Context)


PKEVENT event = (PKEVENT)Context;


// IF this request pended, make sure we mark it as

// having done so in the current IRP stack location


if (Irp->PendingReturned) {

IoMarkIrpPending( Irp );



// Set the event on which the Dispatch Routine is waiting


KeSetEvent(event, 0, FALSE);


// Re-claim IRP to that the Dispatch Routine can continue

// to process it.


// N.B. Dispatch Routine must re-call IoCompleteRequest




Admittedly, this is a pretty simple completion routine. But it does handle all the basics. One step that might not be intuitively obvious to those driver writers who haven?t had a lot of experience passing IRPs around and setting completion routines is the need to call IoMarkIrpPending(...) in the completion routine if Irp->PendingReturned is set. Trust me on this one, boys and girls, this is absolutely required. And, no, you couldn?t do it in the Dispatch Routine code, either. If you?d like an explanation of this, please refer to the article on how I/O completion works in NT published in the May 1997 issue of The NT Insider (which is available on OSR?s web site, of course).

As recommended in the preliminary DDK documentation, the above approach is probably the best method for handling IRPs that need to be processed by the bus driver first. Since it is possible that your completion routine could be called at elevated IRQL, we wait in the Dispatch Routine instead of trying to process the request in the actual completion routine. When the completion routine is called, the IRP is re-claimed by the device driver (by returning STATUS_MORE_PROCESSING_REQUIRED). The completion routine then wakes the Dispatch Routine by setting the event. Any necessary processing is then performed by the device driver in the context of the Dispatch Routine. The device driver then completes the IRP, calling IoCompleteRequest(...), with an appropriate status.



Given the general process for handling IRP_MJ_PNP requests, let?s discuss how you specifically process IRP_MN_START_DEVICE requests.

As we stated previously, when the Plug and Play Manager wants you to start your device, it sends you an IRP_MJ_PNP IRP with an IRP_MN_START_DEVICE minor function. The device to be started is, obviously, the one represented by the Device Object pointer we receive in our Dispatch PNP routine.

Recall that IRP_MN_PNP IRPs are one of those that must be processed by the bus driver before being processed by the device driver. Thus, on receiving one of these IRPs, the device driver simply passes it on down to the bus driver, and waits for its completion routine to be called. Assume we use the design shown above, where we wait in the Dispatch Routine on an event to be signaled by the Completion Routine. In this case, we wake back up in our Dispatch Routine, and proceed to process the IRP_MN_START_DEVICE request.

How do we process this request? Recall that up to this point, we still have neither identified nor reserved the hardware resources required by our device. Providing us a list of these resources is the main purpose of the IRP_MN_START_DEVICE IRP.

Contained in the current I/O Stack Location of the IRP_MN_START_DEVICE IRP are two parameters of specific interest: Parameters.StartDevice.AllocatedResources (which is a pointer to a CM_RESOURCE_LIST that describes the device?s resources), and Parameters.StartDevice.AllocatedResourcesTranslated (which is a pointer to a CM_RESOURCE_LIST which contains the translated values for the device?s resources). These parameters are the resources that the PnP Manager, the I/O Manager, and the HAL have agreed on and allocated for your device?s use. In NT V4 for a PCI device (for example), the untranslated resources are those that would have been returned by HalAssignSlotResources(...). The Translated version of these resources are equivalent to the output from HalTranslateBusAddress(...) and HalGetInterruptVector(...).

Given the CM_RESOURCE_LIST, the driver may access, initialize, and program its device just as in NT V4. And, just like in NT V4, if a resource is in memory space the driver will need to call MmMapIoSpace(...) to assign kernel virtual addresses to it. And, of course, the driver will need to connect to interrupt by calling IoConnectInterrupt(...) just as it did in NT V4.

Summing It All Up


Of course, there?s more we could talk about: How devices get stopped, removed, and the like. But Dan gets mad if these articles are too long, and this should be enough to at least get you started. We?ll be talking more about the emerging NT V5 driver standard, WDM, and how to make your x86 architecture drivers binary compatible between Windows 9x and NT V5 in a future issue.


This article was printed from OSR Online

Copyright 2017 OSR Open Systems Resources, Inc.